@gitkraken/core-gitlens 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/dist/git/cache.d.ts +84 -29
  3. package/dist/git/cache.js +188 -54
  4. package/dist/git/cache.js.map +1 -1
  5. package/dist/git/features.d.ts +1 -1
  6. package/dist/git/features.js +4 -0
  7. package/dist/git/features.js.map +1 -1
  8. package/dist/git/models/branch.d.ts +15 -2
  9. package/dist/git/models/branch.js +27 -5
  10. package/dist/git/models/branch.js.map +1 -1
  11. package/dist/git/models/status.js +1 -1
  12. package/dist/git/models/worktree.d.ts +4 -1
  13. package/dist/git/models/worktree.js +4 -2
  14. package/dist/git/models/worktree.js.map +1 -1
  15. package/dist/git/providers/branches.d.ts +73 -8
  16. package/dist/git/providers/pausedOperations.d.ts +6 -0
  17. package/dist/git/providers/provider.d.ts +34 -2
  18. package/dist/git/providers/status.d.ts +8 -2
  19. package/dist/git/remotes/azure-devops.js +61 -53
  20. package/dist/git/remotes/azure-devops.js.map +1 -1
  21. package/dist/git/service.d.ts +8 -5
  22. package/dist/git/service.js +16 -8
  23. package/dist/git/service.js.map +1 -1
  24. package/dist/git/utils/branch.utils.d.ts +8 -1
  25. package/dist/git/utils/branch.utils.js +30 -1
  26. package/dist/git/utils/branch.utils.js.map +1 -1
  27. package/dist/git/watching/changeEvent.d.ts +13 -2
  28. package/dist/git/watching/changeEvent.js +17 -3
  29. package/dist/git/watching/changeEvent.js.map +1 -1
  30. package/dist/git/watching/gitDirPoller.d.ts +111 -0
  31. package/dist/git/watching/gitDirPoller.js +266 -0
  32. package/dist/git/watching/gitDirPoller.js.map +1 -0
  33. package/dist/git/watching/provider.d.ts +16 -0
  34. package/dist/git/watching/watchGroup.d.ts +40 -4
  35. package/dist/git/watching/watchGroup.js +143 -17
  36. package/dist/git/watching/watchGroup.js.map +1 -1
  37. package/dist/git/watching/watchService.d.ts +70 -1
  38. package/dist/git/watching/watchService.js +158 -13
  39. package/dist/git/watching/watchService.js.map +1 -1
  40. package/dist/git/watching/watchSession.d.ts +44 -0
  41. package/dist/git/watching/watchSession.js +91 -6
  42. package/dist/git/watching/watchSession.js.map +1 -1
  43. package/dist/git/watching/watcherPatterns.d.ts +21 -0
  44. package/dist/git/watching/watcherPatterns.js +67 -1
  45. package/dist/git/watching/watcherPatterns.js.map +1 -1
  46. package/dist/git/watching/worktreeSummary.d.ts +34 -7
  47. package/dist/git/watching/worktreeSummary.js +39 -12
  48. package/dist/git/watching/worktreeSummary.js.map +1 -1
  49. package/dist/git-cli/cliGitProvider.d.ts +34 -2
  50. package/dist/git-cli/cliGitProvider.js +119 -17
  51. package/dist/git-cli/cliGitProvider.js.map +1 -1
  52. package/dist/git-cli/exec/commandClassifier.d.ts +15 -0
  53. package/dist/git-cli/exec/commandClassifier.js +48 -2
  54. package/dist/git-cli/exec/commandClassifier.js.map +1 -1
  55. package/dist/git-cli/exec/git.d.ts +15 -3
  56. package/dist/git-cli/exec/git.js +38 -17
  57. package/dist/git-cli/exec/git.js.map +1 -1
  58. package/dist/git-cli/exec/gitQueue.d.ts +2 -0
  59. package/dist/git-cli/exec/gitQueue.js +2 -1
  60. package/dist/git-cli/exec/gitQueue.js.map +1 -1
  61. package/dist/git-cli/exec/indexLock.d.ts +38 -0
  62. package/dist/git-cli/exec/indexLock.js +150 -0
  63. package/dist/git-cli/exec/indexLock.js.map +1 -0
  64. package/dist/git-cli/parsers/worktreeParser.js +42 -2
  65. package/dist/git-cli/parsers/worktreeParser.js.map +1 -1
  66. package/dist/git-cli/providers/branches.d.ts +109 -10
  67. package/dist/git-cli/providers/branches.js +624 -131
  68. package/dist/git-cli/providers/branches.js.map +1 -1
  69. package/dist/git-cli/providers/config.d.ts +9 -5
  70. package/dist/git-cli/providers/config.js +60 -27
  71. package/dist/git-cli/providers/config.js.map +1 -1
  72. package/dist/git-cli/providers/operations.d.ts +17 -0
  73. package/dist/git-cli/providers/operations.js +153 -96
  74. package/dist/git-cli/providers/operations.js.map +1 -1
  75. package/dist/git-cli/providers/pausedOperations.d.ts +8 -0
  76. package/dist/git-cli/providers/pausedOperations.js +60 -40
  77. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  78. package/dist/git-cli/providers/refs.js +11 -8
  79. package/dist/git-cli/providers/refs.js.map +1 -1
  80. package/dist/git-cli/providers/remotes.js +19 -10
  81. package/dist/git-cli/providers/remotes.js.map +1 -1
  82. package/dist/git-cli/providers/staging.js +40 -15
  83. package/dist/git-cli/providers/staging.js.map +1 -1
  84. package/dist/git-cli/providers/stash.d.ts +2 -0
  85. package/dist/git-cli/providers/stash.js +33 -18
  86. package/dist/git-cli/providers/stash.js.map +1 -1
  87. package/dist/git-cli/providers/status.d.ts +3 -1
  88. package/dist/git-cli/providers/status.js +38 -20
  89. package/dist/git-cli/providers/status.js.map +1 -1
  90. package/dist/git-cli/providers/tags.js +6 -4
  91. package/dist/git-cli/providers/tags.js.map +1 -1
  92. package/dist/git-cli/providers/worktrees.d.ts +1 -0
  93. package/dist/git-cli/providers/worktrees.js +60 -31
  94. package/dist/git-cli/providers/worktrees.js.map +1 -1
  95. package/dist/git-cli/service.d.ts +6 -0
  96. package/dist/git-cli/service.js +5 -2
  97. package/dist/git-cli/service.js.map +1 -1
  98. package/dist/ipc/discovery.js +1 -1
  99. package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
  100. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  101. package/dist/plus/ai/providers/openAICompatibleProviderBase.js +1 -1
  102. package/dist/plus/git-github/providers/github/status.d.ts +3 -1
  103. package/dist/plus/git-github/providers/github/status.js +1 -1
  104. package/dist/plus/git-github/providers/github/status.js.map +1 -1
  105. package/dist/plus/integrations/collectionMetadata.d.ts +13 -2
  106. package/dist/plus/integrations/collectionMetadata.js +14 -2
  107. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  108. package/dist/plus/integrations/index.d.ts +2 -2
  109. package/dist/plus/integrations/index.js.map +1 -1
  110. package/dist/plus/integrations/integrationService.d.ts +1 -0
  111. package/dist/plus/integrations/integrationService.js +1 -1
  112. package/dist/plus/integrations/integrationService.js.map +1 -1
  113. package/dist/plus/integrations/manager.d.ts +1 -0
  114. package/dist/plus/integrations/models/gitHostIntegration.d.ts +41 -5
  115. package/dist/plus/integrations/models/gitHostIntegration.js +106 -29
  116. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  117. package/dist/plus/integrations/models/issueReads.d.ts +17 -4
  118. package/dist/plus/integrations/providers/apiClients.d.ts +2 -0
  119. package/dist/plus/integrations/providers/apiClients.js +7 -1
  120. package/dist/plus/integrations/providers/apiClients.js.map +1 -1
  121. package/dist/plus/integrations/providers/azure/azure.d.ts +5 -4
  122. package/dist/plus/integrations/providers/azure/azure.js +37 -77
  123. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  124. package/dist/plus/integrations/providers/azure/azure.utils.js +20 -17
  125. package/dist/plus/integrations/providers/azure/azure.utils.js.map +1 -1
  126. package/dist/plus/integrations/providers/azure/models.d.ts +2 -2
  127. package/dist/plus/integrations/providers/azure/models.js +4 -4
  128. package/dist/plus/integrations/providers/azure/models.js.map +1 -1
  129. package/dist/plus/integrations/providers/azure/workItemStateCategories.d.ts +18 -14
  130. package/dist/plus/integrations/providers/azure/workItemStateCategories.js +69 -17
  131. package/dist/plus/integrations/providers/azure/workItemStateCategories.js.map +1 -1
  132. package/dist/plus/integrations/providers/azureDevOps.d.ts +44 -0
  133. package/dist/plus/integrations/providers/azureDevOps.js +108 -17
  134. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  135. package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +45 -1
  136. package/dist/plus/integrations/providers/azureDevOpsServer.js +121 -6
  137. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
  138. package/dist/plus/integrations/providers/jira-server.js +18 -14
  139. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  140. package/dist/plus/integrations/providers/jira.js +18 -14
  141. package/dist/plus/integrations/providers/jira.js.map +1 -1
  142. package/dist/plus/integrations/providers/models.d.ts +1 -2
  143. package/dist/plus/integrations/providers/models.js +9 -2
  144. package/dist/plus/integrations/providers/models.js.map +1 -1
  145. package/dist/plus/integrations/providers/providersApi.d.ts +7 -17
  146. package/dist/plus/integrations/providers/providersApi.js +85 -78
  147. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  148. package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +22 -0
  149. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +69 -9
  150. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -1
  151. package/dist/plus/integrations/providers/utils/providerPaging.js +8 -2
  152. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  153. package/dist/plus/integrations/reads/filters.d.ts +3 -3
  154. package/dist/plus/integrations/reads/filters.js.map +1 -1
  155. package/dist/plus/integrations/reads/issueBatch.d.ts +2 -1
  156. package/dist/plus/integrations/reads/issueBatch.js +1 -1
  157. package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
  158. package/dist/plus/integrations/reads/issueTracker.js +23 -14
  159. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  160. package/dist/plus/integrations/reads/issues.d.ts +2 -1
  161. package/dist/plus/integrations/reads/issues.js +18 -3
  162. package/dist/plus/integrations/reads/issues.js.map +1 -1
  163. package/dist/plus/integrations/reads/pullRequestBatch.js +1 -1
  164. package/dist/plus/integrations/reads/resolveRepository.js +12 -3
  165. package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
  166. package/dist/plus/integrations/reads/warnings.d.ts +11 -4
  167. package/dist/plus/integrations/reads/warnings.js +15 -5
  168. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  169. package/dist/plus/integrations/results.d.ts +36 -2
  170. package/dist/plus/integrations/results.js +33 -13
  171. package/dist/plus/integrations/results.js.map +1 -1
  172. package/dist/utils/env/node/exec.js +2 -2
  173. package/dist/utils/promiseCache.d.ts +10 -0
  174. package/dist/utils/promiseCache.js +17 -2
  175. package/dist/utils/promiseCache.js.map +1 -1
  176. package/docs/integrations.md +52 -20
  177. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"jira-server.js","names":[],"sources":["../../../../../plus/integrations/src/providers/jira-server.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport { JIRA_MAX_PROJECT_KEYS_PER_REQUEST } from '@gitkraken/provider-apis';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { Issue, IssueProjection, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport type { Emitter } from '@gitlens/utils/event.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { IntegrationAuthenticationService } from '../authentication/integrationAuthenticationService.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { isIncompleteCollection, toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesSelfManagedHostIntegrationId } from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport type { IntegrationConnectionChangeEvent } from '../integrationService.js';\nimport type { IntegrationKey } from '../models/integration.js';\nimport type {\n\tAccountWideIssuesResult,\n\tIssuesForProjectOptions,\n\tProjectIssuesDrain,\n\tProjectIssuesRequest,\n\tSearchMyIssuesOptions,\n} from '../models/issueReads.js';\nimport { groupProjectIssuesSearches, IssuesIntegration } from '../models/issuesIntegration.js';\nimport { areDomainsOnSameHost, baseUrlFromDomain } from '../utils/domain.utils.js';\nimport { providersMetadata, toAccount, toIssueShape, toProviderIssueStates } from './models.js';\nimport { isJiraMissingProjectError } from './providerErrors.js';\nimport type { ProvidersApi } from './providersApi.js';\nimport { DiscoveryCache } from './utils/discoveryCache.js';\nimport type { JiraDrainOptions, JiraIssuePageFetcher } from './utils/jiraIssueDrains.js';\nimport {\n\tdrainJiraIssues,\n\treadUserScopedJiraIssues,\n\tsearchJiraProjectIssues,\n\tsummarizePageMetadata,\n\ttoUniqueJiraIssueShapes,\n\twithFailureScope,\n} from './utils/jiraIssueDrains.js';\nimport { mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesSelfManagedHostIntegrationId.JiraServer];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n\n/**\n * Every clause is scoped by the HANDLE, and `options.userId` is deliberately ignored here — the one\n * place this provider diverges from Jira Cloud's, which applies it to `assignee`/`creator` (#5857).\n *\n * The two are not interchangeable because `Account.id` means something different per deployment. On\n * Cloud it is the `accountId`, which JQL resolves, so scoping by it fixes the deactivated-account case\n * where the display name no longer resolves. On Server it is the user KEY (`JIRAUSER10224`), which JQL\n * does not resolve at all: measured against a live Jira Server 8.20.13 site, `assignee in\n * (\"JIRAUSER10224\")` returns 0 with `\"The value 'JIRAUSER10224' does not exist for the field\n * 'assignee'\"` — the identical warning a genuinely nonexistent user produces — while the username and\n * the display name both return the real 4.\n *\n * So applying `userId` here would silently empty the read rather than harden it. Do not \"fix\" this\n * asymmetry to match Cloud without re-measuring against a Server instance.\n *\n * The handle itself is already the right one: `provider-apis` 0.60.0 maps `Account.username` from the\n * Server payload's `name` (falling back to `displayName` when absent), so these clauses are scoped by the\n * real username rather than a display name (GKDEV-3629). That is what makes the `mention` clause work at\n * all — it is a free-text `comment ~ \"...\"` over comment bodies, and a Jira mention is stored as\n * `[~username]`, which a display name never matches.\n */\nfunction toUserIdentities(user: string): { userField: string; mention: string } {\n\treturn { userField: user, mention: user };\n}\n\n/**\n * The single resource a Jira Server connection has: the instance itself.\n *\n * Jira Cloud's account spans many sites, so its resources come from an API call that lists them. A\n * self-hosted instance IS the one site the connection addresses, so there is nothing to enumerate — this\n * descriptor is synthesized from the configured host. Modelling it as a resource anyway keeps the\n * resource → project contract of {@link IssuesIntegration} intact, so every tracker read (`listOrgs`,\n * `listProjects`, `listIssueTrackerIssuesPage`) works unchanged.\n */\nexport interface JiraServerResourceDescriptor extends IssueResourceDescriptor {\n\turl: string;\n}\n\nexport interface JiraServerProjectDescriptor extends IssueResourceDescriptor {\n\tresourceId: string;\n}\n\nexport class JiraServerIntegration extends IssuesIntegration<IssuesSelfManagedHostIntegrationId.JiraServer> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\treadonly id = IssuesSelfManagedHostIntegrationId.JiraServer;\n\tprotected readonly key: IntegrationKey<IssuesSelfManagedHostIntegrationId.JiraServer>;\n\treadonly name: string = 'Jira Data Center';\n\n\tconstructor(\n\t\tctx: IntegrationServiceContext,\n\t\tauthenticationService: IntegrationAuthenticationService,\n\t\tgetProvidersApi: () => Promise<ProvidersApi>,\n\t\tdidChangeConnection: Emitter<IntegrationConnectionChangeEvent>,\n\t\tprivate readonly _domain: string,\n\t) {\n\t\tsuper(ctx, authenticationService, getProvidersApi, didChangeConnection);\n\t\tthis.key = `${this.id}:${this._domain}`;\n\t}\n\n\tget domain(): string {\n\t\treturn this._domain;\n\t}\n\n\t/**\n\t * The URL every request for this connection is addressed to, built from the SESSION's domain rather than\n\t * the integration's.\n\t *\n\t * `this.domain` is the host-keyed identity — `IntegrationService.get()` runs it through `hostFromDomain`\n\t * so one host can't build two instances — which means it has had any context path stripped, and so has\n\t * `session.domain`, which is stored as that same normalized host. Jira Data Center officially supports\n\t * being mounted below a context path (`https://jira.example.com/jira`), so the address comes from\n\t * {@link ProviderAuthenticationSession.baseUrl}, which carries the configured value verbatim; it falls\n\t * back to the host when the backend reported nothing more, which is the common case.\n\t *\n\t * Takes the session rather than reading `_session` so a per-connection (multi-account) read is addressed\n\t * to the host of the connection it resolved, not to whichever one happens to be current.\n\t */\n\tprotected apiBaseUrlFor(session: ProviderAuthenticationSession | undefined): string {\n\t\tconst configured = baseUrlFromDomain(session?.baseUrl, session?.protocol);\n\t\t// A `baseUrl` from another host is not this connection's address — refuse it rather than routing this\n\t\t// integration's reads somewhere its key never named.\n\t\tif (configured != null && areDomainsOnSameHost(session?.baseUrl, this.domain)) return configured;\n\n\t\treturn (\n\t\t\tbaseUrlFromDomain(session?.domain, session?.protocol) ??\n\t\t\tbaseUrlFromDomain(this.domain, session?.protocol) ??\n\t\t\t`${session?.protocol ?? 'https:'}//${this.domain}`\n\t\t);\n\t}\n\n\t/** The one resource this connection has — see {@link JiraServerResourceDescriptor}. */\n\tprivate resourceFor(session: ProviderAuthenticationSession | undefined): JiraServerResourceDescriptor {\n\t\treturn { id: this.domain, key: this.domain, name: this.domain, url: this.apiBaseUrlFor(session) };\n\t}\n\n\t// No `autolinks()` override yet, unlike Jira Cloud: an autolink prefix has to be the Jira project KEY, which\n\t// is what a commit message or branch name carries (`PROJ-123`). The project descriptors carry it now (see\n\t// `getProviderProjectsForResources`), so the override can follow Cloud's; until then none is registered.\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: JiraServerResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getJiraServerCurrentUser(toTokenWithInfo(this.id, session), this.apiBaseUrlFor(session));\n\n\t\tif (user == null) return undefined;\n\t\treturn toAccount(user, this);\n\t}\n\n\tprotected override getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_force?: boolean,\n\t): Promise<JiraServerResourceDescriptor[] | undefined> {\n\t\t// Synthesized from the configured host rather than fetched, so there is nothing to cache or force.\n\t\treturn Promise.resolve([this.resourceFor(session)]);\n\t}\n\n\t/** Expires, and is dropped on a re-sync, so a project created or deleted mid-session is picked up (#5907). */\n\tprivate readonly _projects = new DiscoveryCache<JiraServerProjectDescriptor[]>();\n\tprotected override async getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraServerResourceDescriptor[],\n\t\tforce: boolean = false,\n\t): Promise<JiraServerProjectDescriptor[] | undefined> {\n\t\t// Every resource here is this instance (see `getProviderResourcesForUser`), so one read serves them\n\t\t// all; a caller that passes none is asking for nothing.\n\t\tif (resources.length === 0) return [];\n\n\t\tconst cacheKey = this.projectsCacheKey(session);\n\t\tconst cached = this._projects.get(cacheKey);\n\t\tif (cached != null && !force) return cached;\n\n\t\tconst generation = this._projects.generation;\n\t\tconst api = await this.getProvidersApi();\n\t\tconst projects = await api.getJiraServerProjects(\n\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\tthis.apiBaseUrlFor(session),\n\t\t);\n\t\tif (projects == null) return undefined;\n\n\t\tconst resourceId = this.resourceFor(session).id;\n\t\tconst descriptors = projects.map<JiraServerProjectDescriptor>(p => ({\n\t\t\tid: p.id,\n\t\t\t// The project key, which selects the project (`listProjects`' `name`) and is what an issue key starts\n\t\t\t// with. The SDK does not validate it, so a missing or blank one falls back to the numeric id, never to the\n\t\t\t// display name, which can equal another project's key. The reads below still address the project by\n\t\t\t// `id`, which no other project can share.\n\t\t\tkey: p.key?.trim() || p.id,\n\t\t\tname: p.name,\n\t\t\tresourceId: resourceId,\n\t\t}));\n\n\t\tthis._projects.set(cacheKey, descriptors, { generation: generation });\n\n\t\treturn descriptors;\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraServerProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraServerProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\n\t\t// The same drain as Jira Cloud's (`jiraIssueDrains.ts`): page to a defensive backstop, and report `truncated`\n\t\t// plus a structured failure rather than discarding an already-fetched prefix.\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\t// The numeric project id, not the name: JQL resolves `project = \"<value>\"` against key first, so a display\n\t\t// name that matches another project's key reads that project's issues instead. A Jira project key must begin\n\t\t// with a letter, so a purely numeric value can only be an id.\n\t\tconst fetchPage: JiraIssuePageFetcher = (scope, cursor) =>\n\t\t\tapi.getJiraServerIssuesForProjectPaged(tokenWithInfo, baseUrl, project.id, {\n\t\t\t\t...scope,\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t\tsort: options?.sort,\n\t\t\t});\n\t\tconst drainOptions: JiraDrainOptions = {\n\t\t\tmaxPages: maxPagesPerRequest,\n\t\t\tfailureScope: { providerId: this.id, resourceId: project.resourceId, projectId: project.id },\n\t\t\tproviderName: 'Jira Server',\n\t\t\t// A project this connection can no longer see (deleted, or its browse permission removed) holds\n\t\t\t// no issues for it, and the project list that named it is stale: drop that list so the next read\n\t\t\t// re-reads it, instead of failing the whole provider on every read until a restart (#5907).\n\t\t\tisEmptyFailure: ex => {\n\t\t\t\tif (!isJiraMissingProjectError(ex)) return false;\n\n\t\t\t\tLogger.warn(`Jira Server project '${project.name}' (${project.id}) no longer exists or is not visible`);\n\t\t\t\tthis._projects.delete(this.projectsCacheKey(session));\n\t\t\t\treturn true;\n\t\t\t},\n\t\t};\n\n\t\tif (options?.user != null) {\n\t\t\tconst read = await readUserScopedJiraIssues(\n\t\t\t\tfetchPage,\n\t\t\t\toptions.filters,\n\t\t\t\ttoUserIdentities(options.user),\n\t\t\t\tdrainOptions,\n\t\t\t);\n\t\t\tconst values = toUniqueJiraIssueShapes(read.issues, this);\n\t\t\t// `recovery: 'none'` for the whole set: the filter branches merge here, so even a pure backstop in one\n\t\t\t// branch is not fixable by narrowing this read's scope.\n\t\t\treturn read.truncated\n\t\t\t\t? { values: values, truncated: true, recovery: 'none', metadata: read.metadata }\n\t\t\t\t: { values: values, truncated: false, metadata: read.metadata };\n\t\t}\n\n\t\tconst unscoped = await drainJiraIssues(fetchPage, {}, drainOptions);\n\t\tconst values = unscoped.issues\n\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'project' }))\n\t\t\t.filter((result): result is IssueShape => result !== undefined);\n\t\treturn unscoped.status !== 'complete'\n\t\t\t? {\n\t\t\t\t\tvalues: values,\n\t\t\t\t\ttruncated: true,\n\t\t\t\t\trecovery: unscoped.status === 'backstop' ? 'narrow-scope' : 'none',\n\t\t\t\t\tmetadata: unscoped.metadata,\n\t\t\t\t}\n\t\t\t: { values: values, truncated: false, metadata: unscoped.metadata };\n\t}\n\n\t/**\n\t * Searches the instance's projects together for a user-scoped read, as Jira Cloud does per site: one JQL names\n\t * up to {@link JIRA_MAX_PROJECT_KEYS_PER_REQUEST} projects of one user scope. An unscoped read stays per project,\n\t * since its backstop is reported as `narrow-scope` per project.\n\t */\n\tprotected override getProjectIssuesSearches(\n\t\trequests: readonly ProjectIssuesRequest<JiraServerProjectDescriptor>[],\n\t): number[][] | undefined {\n\t\treturn groupProjectIssuesSearches(\n\t\t\trequests,\n\t\t\t({ options }) =>\n\t\t\t\toptions.user != null\n\t\t\t\t\t? [options.user, options.sort ?? '', options.state ?? '', options.filters?.join(',') ?? ''].join(\n\t\t\t\t\t\t\t'\\0',\n\t\t\t\t\t\t)\n\t\t\t\t\t: undefined,\n\t\t\tJIRA_MAX_PROJECT_KEYS_PER_REQUEST,\n\t\t);\n\t}\n\n\tprotected override async searchProviderProjectIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\trequests: readonly ProjectIssuesRequest<JiraServerProjectDescriptor>[],\n\t): Promise<IssueShape[][] | undefined> {\n\t\t// Every request of a search shares its user scope (see `getProjectIssuesSearches`).\n\t\tconst { options } = requests[0];\n\t\tif (options.user == null) return undefined;\n\t\t// One project's search is its own read, by the same numeric id, so it costs no more to read it as one. And it\n\t\t// reports what the search can't: a dropped issue's failure, which a search can only answer by being read again.\n\t\tif (requests.length === 1) return undefined;\n\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\t\t// By numeric id, as the per-project read names them (see there).\n\t\tconst projectIds = requests.map(request => request.project.id);\n\t\tconst states = toProviderIssueStates(options.state);\n\n\t\treturn searchJiraProjectIssues(\n\t\t\t(userScope, cursor) =>\n\t\t\t\tapi.getJiraServerIssuesForProjectsPaged(tokenWithInfo, baseUrl, projectIds, {\n\t\t\t\t\t...userScope,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tstates: states,\n\t\t\t\t\tsort: options.sort,\n\t\t\t\t}),\n\t\t\tprojectIds,\n\t\t\toptions.filters,\n\t\t\ttoUserIdentities(options.user),\n\t\t\t{ maxPages: maxPagesPerRequest, failureScope: { providerId: this.id }, providerName: 'Jira Server' },\n\t\t\tthis,\n\t\t);\n\t}\n\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: JiraServerResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation))?.values;\n\t}\n\n\t/**\n\t * Account-wide \"my issues\" for this instance, drained to the backstop and reporting whether it got there.\n\t *\n\t * The override exists for the same reason the project drain reports `status`: the default\n\t * implementation wraps whatever the normalized read returned as `truncated: false`, so a run that stopped\n\t * at `maxPagesPerRequest` with pages left, or on a continuation the server never advanced, would be\n\t * published as a complete account. Cancellation is NOT truncation — the caller asked for the stop and\n\t * already knows the prefix is partial — so it breaks without setting the flag.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources?: JiraServerResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\n\t\t// Errors propagate, unlike Jira Cloud's sibling. Cloud swallows per resource because it loops over the\n\t\t// account's many sites and one bad site must not discard the others; this connection addresses exactly\n\t\t// one instance, so there is no sibling to protect and swallowing would turn every failure — an expired\n\t\t// token, an unreachable host — into an empty success. `searchMyIssuesWithTruncationResult` recovers the\n\t\t// throw into `{ error }`, which is what lets the caller warn instead of reporting \"no issues\".\n\t\t// Only the provider: the instance is this connection's single resource, so naming it would publish a\n\t\t// failure of the whole connection as one confined to a sub-scope (see `ProviderWarning.scope`).\n\t\tconst accountScope = { providerId: this.id };\n\t\tconst results: IssueShape[] = [];\n\t\tlet cursor: string | undefined;\n\t\tlet truncated = false;\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\tconst pages: CollectionMetadata[] = [];\n\n\t\tconst stopIncomplete = (reason: string) => {\n\t\t\ttruncated = true;\n\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\tcompleteness: 'partial',\n\t\t\t\tfailures: [toCollectionScopeFailure(accountScope, new Error(reason))],\n\t\t\t});\n\t\t};\n\n\t\tfor (let i = 0; i < maxPagesPerRequest; i++) {\n\t\t\tif (cancellation?.aborted) break;\n\n\t\t\tconst page = await api.getJiraServerIssuesForCurrentUser(tokenWithInfo, baseUrl, {\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t});\n\t\t\tif (page == null) {\n\t\t\t\t// Nothing fetched yet and no page is an empty account, not a failure; after a continuation it\n\t\t\t\t// means the server dropped the drain mid-way.\n\t\t\t\tif (cursor != null) {\n\t\t\t\t\tstopIncomplete('Jira Server returned no page after advertising a continuation');\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tresults.push(\n\t\t\t\t...page.data\n\t\t\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'account' }))\n\t\t\t\t\t.filter((r): r is IssueShape => r != null),\n\t\t\t);\n\t\t\t// An issue the page dropped because it could not be read arrives here, not as a missing row; recorded under\n\t\t\t// the same scope as this read's other failures.\n\t\t\tif (page.metadata != null) {\n\t\t\t\t// Truncated too, as Jira Cloud's read is through `getPagedResult`: the account is missing that issue.\n\t\t\t\ttruncated ||= isIncompleteCollection(page.metadata);\n\t\t\t\tpages.push(withFailureScope(page.metadata, accountScope));\n\t\t\t}\n\n\t\t\tif (!page.hasMore) break;\n\n\t\t\t// More pages are claimed but no advancing cursor came back: the drain can't continue, so flag it\n\t\t\t// rather than stopping silently.\n\t\t\tif (page.nextCursor == null || page.nextCursor === cursor) {\n\t\t\t\tstopIncomplete('Jira Server returned no advancing issue continuation');\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcursor = page.nextCursor;\n\t\t\t// Pages remain but this was the last allowed iteration: the backstop stopped the drain.\n\t\t\tif (i === maxPagesPerRequest - 1) {\n\t\t\t\tstopIncomplete(\n\t\t\t\t\t`Jira Server account-wide issue drain stopped at the ${maxPagesPerRequest}-page backstop`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tvalues: results,\n\t\t\ttruncated: truncated,\n\t\t\tmetadata: mergeCollectionMetadata(summarizePageMetadata(pages), metadata),\n\t\t};\n\t}\n\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: JiraServerResourceDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst issue = await api.getJiraServerIssue(toTokenWithInfo(this.id, session), this.apiBaseUrlFor(session), key);\n\t\treturn issue != null ? toIssueShape(issue, this, { projection: 'point' }) : undefined;\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: JiraServerResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\treturn this.readIssue(session, id, 'point');\n\t}\n\n\tprivate async readIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tid: string,\n\t\tprojection: IssueProjection,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst apiResult = await api.getJiraServerIssue(\n\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\tthis.apiBaseUrlFor(session),\n\t\t\tid,\n\t\t);\n\t\tconst issue = apiResult != null ? toIssueShape(apiResult, this, { projection: projection }) : undefined;\n\t\treturn issue != null ? { ...issue, type: 'issue' } : undefined;\n\t}\n\n\t/**\n\t * The point read behind `getIssuesBatch`'s tracker targets (#5872). `resourceId` is this connection's single\n\t * synthetic resource — the host itself, see {@link JiraServerResourceDescriptor} — so it selects nothing further\n\t * here: the host was chosen upstream by `domain` (or the connection's configured domain) and the request is\n\t * addressed to the SESSION's base URL like every other read. No `resourceUrl` is needed either, unlike Jira\n\t * Cloud: its REST `self` link is an API endpoint, whereas `getJiraServerIssue` builds the browser link from\n\t * the base URL it was addressed to. A key that names no issue resolves to `undefined`, a proven absence.\n\t */\n\tprotected override getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resourceId: string,\n\t\tid: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\treturn this.readIssue(session, id, 'batch');\n\t}\n\n\t/**\n\t * Caches are keyed by the ADDRESS the read was made against as well as the token: one integration instance\n\t * exists per configured host and two hosts can hold the same token, so a token-only key would serve one\n\t * host's projects for the other — and the host alone is not enough either, because a connection re-pointed\n\t * to another context path on the same host keeps its token and would otherwise be served the old path's\n\t * projects.\n\t */\n\tprivate projectsCacheKey(session: ProviderAuthenticationSession): string {\n\t\treturn `${this.apiBaseUrlFor(session)}:${session.accessToken}`;\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._projects.clear();\n\t}\n\n\tprotected override providerOnDisconnect(): void {\n\t\tthis._projects.clear();\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"jira-server.js","names":[],"sources":["../../../../../plus/integrations/src/providers/jira-server.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport { JIRA_MAX_PROJECT_KEYS_PER_REQUEST } from '@gitkraken/provider-apis';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { Issue, IssueProjection, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport type { Emitter } from '@gitlens/utils/event.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { IntegrationAuthenticationService } from '../authentication/integrationAuthenticationService.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { isIncompleteCollection, toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesSelfManagedHostIntegrationId } from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport type { IntegrationConnectionChangeEvent } from '../integrationService.js';\nimport type { IntegrationKey } from '../models/integration.js';\nimport type {\n\tAccountWideIssuesResult,\n\tIssuesForProjectOptions,\n\tProjectIssuesDrain,\n\tProjectIssuesRequest,\n\tSearchMyIssuesOptions,\n} from '../models/issueReads.js';\nimport { groupProjectIssuesSearches, IssuesIntegration } from '../models/issuesIntegration.js';\nimport { areDomainsOnSameHost, baseUrlFromDomain } from '../utils/domain.utils.js';\nimport { providersMetadata, toAccount, toIssueShape, toProviderIssueStates } from './models.js';\nimport { isJiraMissingProjectError } from './providerErrors.js';\nimport type { ProvidersApi } from './providersApi.js';\nimport { DiscoveryCache } from './utils/discoveryCache.js';\nimport type { JiraDrainOptions, JiraIssuePageFetcher } from './utils/jiraIssueDrains.js';\nimport {\n\tdrainJiraIssues,\n\treadUserScopedJiraIssues,\n\tsearchJiraProjectIssues,\n\tsummarizePageMetadata,\n\ttoUniqueJiraIssueShapes,\n\twithFailureScope,\n} from './utils/jiraIssueDrains.js';\nimport { mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesSelfManagedHostIntegrationId.JiraServer];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n\n/**\n * Every clause is scoped by the HANDLE, and `options.userId` is deliberately ignored here — the one\n * place this provider diverges from Jira Cloud's, which applies it to `assignee`/`creator` (#5857).\n *\n * The two are not interchangeable because `Account.id` means something different per deployment. On\n * Cloud it is the `accountId`, which JQL resolves, so scoping by it fixes the deactivated-account case\n * where the display name no longer resolves. On Server it is the user KEY (`JIRAUSER10224`), which JQL\n * does not resolve at all: measured against a live Jira Server 8.20.13 site, `assignee in\n * (\"JIRAUSER10224\")` returns 0 with `\"The value 'JIRAUSER10224' does not exist for the field\n * 'assignee'\"` — the identical warning a genuinely nonexistent user produces — while the username and\n * the display name both return the real 4.\n *\n * So applying `userId` here would silently empty the read rather than harden it. Do not \"fix\" this\n * asymmetry to match Cloud without re-measuring against a Server instance.\n *\n * The handle itself is already the right one: `provider-apis` 0.60.0 maps `Account.username` from the\n * Server payload's `name` (falling back to `displayName` when absent), so these clauses are scoped by the\n * real username rather than a display name (GKDEV-3629). That is what makes the `mention` clause work at\n * all — it is a free-text `comment ~ \"...\"` over comment bodies, and a Jira mention is stored as\n * `[~username]`, which a display name never matches.\n */\nfunction toUserIdentities(user: string): { userField: string; mention: string } {\n\treturn { userField: user, mention: user };\n}\n\n/**\n * The single resource a Jira Server connection has: the instance itself.\n *\n * Jira Cloud's account spans many sites, so its resources come from an API call that lists them. A\n * self-hosted instance IS the one site the connection addresses, so there is nothing to enumerate — this\n * descriptor is synthesized from the configured host. Modelling it as a resource anyway keeps the\n * resource → project contract of {@link IssuesIntegration} intact, so every tracker read (`listOrgs`,\n * `listProjects`, `listIssueTrackerIssuesPage`) works unchanged.\n */\nexport interface JiraServerResourceDescriptor extends IssueResourceDescriptor {\n\turl: string;\n}\n\nexport interface JiraServerProjectDescriptor extends IssueResourceDescriptor {\n\tresourceId: string;\n}\n\nexport class JiraServerIntegration extends IssuesIntegration<IssuesSelfManagedHostIntegrationId.JiraServer> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\treadonly id = IssuesSelfManagedHostIntegrationId.JiraServer;\n\tprotected readonly key: IntegrationKey<IssuesSelfManagedHostIntegrationId.JiraServer>;\n\treadonly name: string = 'Jira Data Center';\n\n\tconstructor(\n\t\tctx: IntegrationServiceContext,\n\t\tauthenticationService: IntegrationAuthenticationService,\n\t\tgetProvidersApi: () => Promise<ProvidersApi>,\n\t\tdidChangeConnection: Emitter<IntegrationConnectionChangeEvent>,\n\t\tprivate readonly _domain: string,\n\t) {\n\t\tsuper(ctx, authenticationService, getProvidersApi, didChangeConnection);\n\t\tthis.key = `${this.id}:${this._domain}`;\n\t}\n\n\tget domain(): string {\n\t\treturn this._domain;\n\t}\n\n\t/**\n\t * The URL every request for this connection is addressed to, built from the SESSION's domain rather than\n\t * the integration's.\n\t *\n\t * `this.domain` is the host-keyed identity — `IntegrationService.get()` runs it through `hostFromDomain`\n\t * so one host can't build two instances — which means it has had any context path stripped, and so has\n\t * `session.domain`, which is stored as that same normalized host. Jira Data Center officially supports\n\t * being mounted below a context path (`https://jira.example.com/jira`), so the address comes from\n\t * {@link ProviderAuthenticationSession.baseUrl}, which carries the configured value verbatim; it falls\n\t * back to the host when the backend reported nothing more, which is the common case.\n\t *\n\t * Takes the session rather than reading `_session` so a per-connection (multi-account) read is addressed\n\t * to the host of the connection it resolved, not to whichever one happens to be current.\n\t */\n\tprotected apiBaseUrlFor(session: ProviderAuthenticationSession | undefined): string {\n\t\tconst configured = baseUrlFromDomain(session?.baseUrl, session?.protocol);\n\t\t// A `baseUrl` from another host is not this connection's address — refuse it rather than routing this\n\t\t// integration's reads somewhere its key never named.\n\t\tif (configured != null && areDomainsOnSameHost(session?.baseUrl, this.domain)) return configured;\n\n\t\treturn (\n\t\t\tbaseUrlFromDomain(session?.domain, session?.protocol) ??\n\t\t\tbaseUrlFromDomain(this.domain, session?.protocol) ??\n\t\t\t`${session?.protocol ?? 'https:'}//${this.domain}`\n\t\t);\n\t}\n\n\t/** The one resource this connection has — see {@link JiraServerResourceDescriptor}. */\n\tprivate resourceFor(session: ProviderAuthenticationSession | undefined): JiraServerResourceDescriptor {\n\t\treturn { id: this.domain, key: this.domain, name: this.domain, url: this.apiBaseUrlFor(session) };\n\t}\n\n\t// No `autolinks()` override yet, unlike Jira Cloud: an autolink prefix has to be the Jira project KEY, which\n\t// is what a commit message or branch name carries (`PROJ-123`). The project descriptors carry it now (see\n\t// `getProviderProjectsForResources`), so the override can follow Cloud's; until then none is registered.\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: JiraServerResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getJiraServerCurrentUser(toTokenWithInfo(this.id, session), this.apiBaseUrlFor(session));\n\n\t\tif (user == null) return undefined;\n\t\treturn toAccount(user, this);\n\t}\n\n\tprotected override getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_force?: boolean,\n\t): Promise<JiraServerResourceDescriptor[] | undefined> {\n\t\t// Synthesized from the configured host rather than fetched, so there is nothing to cache or force.\n\t\treturn Promise.resolve([this.resourceFor(session)]);\n\t}\n\n\t/** Expires, and is dropped on a re-sync, so a project created or deleted mid-session is picked up (#5907). */\n\tprivate readonly _projects = new DiscoveryCache<JiraServerProjectDescriptor[]>();\n\tprotected override async getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraServerResourceDescriptor[],\n\t\tforce: boolean = false,\n\t): Promise<JiraServerProjectDescriptor[] | undefined> {\n\t\t// Every resource here is this instance (see `getProviderResourcesForUser`), so one read serves them\n\t\t// all; a caller that passes none is asking for nothing.\n\t\tif (resources.length === 0) return [];\n\n\t\tconst cacheKey = this.projectsCacheKey(session);\n\t\tconst cached = this._projects.get(cacheKey);\n\t\tif (cached != null && !force) return cached;\n\n\t\tconst generation = this._projects.generation;\n\t\tconst api = await this.getProvidersApi();\n\t\tconst projects = await api.getJiraServerProjects(\n\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\tthis.apiBaseUrlFor(session),\n\t\t);\n\t\tif (projects == null) return undefined;\n\n\t\tconst resourceId = this.resourceFor(session).id;\n\t\tconst descriptors = projects.map<JiraServerProjectDescriptor>(p => ({\n\t\t\tid: p.id,\n\t\t\t// The project key, which selects the project (`listProjects`' `name`) and is what an issue key starts\n\t\t\t// with. The SDK does not validate it, so a missing or blank one falls back to the numeric id, never to the\n\t\t\t// display name, which can equal another project's key. The reads below still address the project by\n\t\t\t// `id`, which no other project can share.\n\t\t\tkey: p.key?.trim() || p.id,\n\t\t\tname: p.name,\n\t\t\tresourceId: resourceId,\n\t\t}));\n\n\t\tthis._projects.set(cacheKey, descriptors, { generation: generation });\n\n\t\treturn descriptors;\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraServerProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraServerProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\n\t\t// The same drain as Jira Cloud's (`jiraIssueDrains.ts`): page to a defensive backstop, and report `truncated`\n\t\t// plus a structured failure rather than discarding an already-fetched prefix.\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\t// The numeric project id, not the name: JQL resolves `project = \"<value>\"` against key first, so a display\n\t\t// name that matches another project's key reads that project's issues instead. A Jira project key must begin\n\t\t// with a letter, so a purely numeric value can only be an id.\n\t\tconst fetchPage: JiraIssuePageFetcher = (scope, cursor) =>\n\t\t\tapi.getJiraServerIssuesForProjectPaged(tokenWithInfo, baseUrl, project.id, {\n\t\t\t\t...scope,\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t\tsort: options?.sort,\n\t\t\t});\n\t\tconst drainOptions: JiraDrainOptions = {\n\t\t\tmaxPages: maxPagesPerRequest,\n\t\t\tfailureScope: { providerId: this.id, resourceId: project.resourceId, projectId: project.id },\n\t\t\tproviderName: 'Jira Server',\n\t\t\t// A project this connection can no longer see (deleted, or its browse permission removed) holds\n\t\t\t// no issues for it, and the project list that named it is stale: drop that list so the next read\n\t\t\t// re-reads it, instead of failing the whole provider on every read until a restart (#5907).\n\t\t\tisEmptyFailure: ex => {\n\t\t\t\tif (!isJiraMissingProjectError(ex)) return false;\n\n\t\t\t\tLogger.warn(`Jira Server project '${project.name}' (${project.id}) no longer exists or is not visible`);\n\t\t\t\tthis._projects.delete(this.projectsCacheKey(session));\n\t\t\t\treturn true;\n\t\t\t},\n\t\t};\n\n\t\tif (options?.user != null) {\n\t\t\tconst read = await readUserScopedJiraIssues(\n\t\t\t\tfetchPage,\n\t\t\t\toptions.filters,\n\t\t\t\ttoUserIdentities(options.user),\n\t\t\t\tdrainOptions,\n\t\t\t);\n\t\t\tconst values = toUniqueJiraIssueShapes(read.issues, this);\n\t\t\tconst drain = { values: values, metadata: read.metadata, scope: drainOptions.failureScope };\n\t\t\t// `recovery: 'none'` for the whole set: the filter branches merge here, so even a pure backstop in one\n\t\t\t// branch is not fixable by narrowing this read's scope.\n\t\t\treturn read.truncated ? { ...drain, truncated: true, recovery: 'none' } : { ...drain, truncated: false };\n\t\t}\n\n\t\tconst unscoped = await drainJiraIssues(fetchPage, {}, drainOptions);\n\t\tconst values = unscoped.issues\n\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'project' }))\n\t\t\t.filter((result): result is IssueShape => result !== undefined);\n\t\tconst drain = { values: values, metadata: unscoped.metadata, scope: drainOptions.failureScope };\n\t\treturn unscoped.status !== 'complete'\n\t\t\t? { ...drain, truncated: true, recovery: unscoped.status === 'backstop' ? 'narrow-scope' : 'none' }\n\t\t\t: { ...drain, truncated: false };\n\t}\n\n\t/**\n\t * Searches the instance's projects together for a user-scoped read, as Jira Cloud does per site: one JQL names\n\t * up to {@link JIRA_MAX_PROJECT_KEYS_PER_REQUEST} projects of one user scope. An unscoped read stays per project,\n\t * since its backstop is reported as `narrow-scope` per project.\n\t */\n\tprotected override getProjectIssuesSearches(\n\t\trequests: readonly ProjectIssuesRequest<JiraServerProjectDescriptor>[],\n\t): number[][] | undefined {\n\t\treturn groupProjectIssuesSearches(\n\t\t\trequests,\n\t\t\t({ options }) =>\n\t\t\t\toptions.user != null\n\t\t\t\t\t? [options.user, options.sort ?? '', options.state ?? '', options.filters?.join(',') ?? ''].join(\n\t\t\t\t\t\t\t'\\0',\n\t\t\t\t\t\t)\n\t\t\t\t\t: undefined,\n\t\t\tJIRA_MAX_PROJECT_KEYS_PER_REQUEST,\n\t\t);\n\t}\n\n\tprotected override async searchProviderProjectIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\trequests: readonly ProjectIssuesRequest<JiraServerProjectDescriptor>[],\n\t): Promise<IssueShape[][] | undefined> {\n\t\t// Every request of a search shares its user scope (see `getProjectIssuesSearches`).\n\t\tconst { options } = requests[0];\n\t\tif (options.user == null) return undefined;\n\t\t// One project's search is its own read, by the same numeric id, so it costs no more to read it as one. And it\n\t\t// reports what the search can't: a dropped issue's failure, which a search can only answer by being read again.\n\t\tif (requests.length === 1) return undefined;\n\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\t\t// By numeric id, as the per-project read names them (see there).\n\t\tconst projectIds = requests.map(request => request.project.id);\n\t\tconst states = toProviderIssueStates(options.state);\n\n\t\treturn searchJiraProjectIssues(\n\t\t\t(userScope, cursor) =>\n\t\t\t\tapi.getJiraServerIssuesForProjectsPaged(tokenWithInfo, baseUrl, projectIds, {\n\t\t\t\t\t...userScope,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tstates: states,\n\t\t\t\t\tsort: options.sort,\n\t\t\t\t}),\n\t\t\tprojectIds,\n\t\t\toptions.filters,\n\t\t\ttoUserIdentities(options.user),\n\t\t\t{ maxPages: maxPagesPerRequest, failureScope: { providerId: this.id }, providerName: 'Jira Server' },\n\t\t\tthis,\n\t\t);\n\t}\n\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: JiraServerResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation))?.values;\n\t}\n\n\t/**\n\t * Account-wide \"my issues\" for this instance, drained to the backstop and reporting whether it got there.\n\t *\n\t * The override exists for the same reason the project drain reports `status`: the default\n\t * implementation wraps whatever the normalized read returned as `truncated: false`, so a run that stopped\n\t * at `maxPagesPerRequest` with pages left, or on a continuation the server never advanced, would be\n\t * published as a complete account. Cancellation is NOT truncation — the caller asked for the stop and\n\t * already knows the prefix is partial — so it breaks without setting the flag.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources?: JiraServerResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\n\t\t// Errors propagate, unlike Jira Cloud's sibling. Cloud swallows per resource because it loops over the\n\t\t// account's many sites and one bad site must not discard the others; this connection addresses exactly\n\t\t// one instance, so there is no sibling to protect and swallowing would turn every failure — an expired\n\t\t// token, an unreachable host — into an empty success. `searchMyIssuesWithTruncationResult` recovers the\n\t\t// throw into `{ error }`, which is what lets the caller warn instead of reporting \"no issues\".\n\t\t// Only the provider: the instance is this connection's single resource, so naming it would publish a\n\t\t// failure of the whole connection as one confined to a sub-scope (see `ProviderWarning.scope`).\n\t\tconst accountScope = { providerId: this.id };\n\t\tconst results: IssueShape[] = [];\n\t\tlet cursor: string | undefined;\n\t\tlet truncated = false;\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\tconst pages: CollectionMetadata[] = [];\n\n\t\tconst stopIncomplete = (reason: string) => {\n\t\t\ttruncated = true;\n\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\tcompleteness: 'partial',\n\t\t\t\tfailures: [toCollectionScopeFailure(accountScope, new Error(reason))],\n\t\t\t});\n\t\t};\n\n\t\tfor (let i = 0; i < maxPagesPerRequest; i++) {\n\t\t\tif (cancellation?.aborted) break;\n\n\t\t\tconst page = await api.getJiraServerIssuesForCurrentUser(tokenWithInfo, baseUrl, {\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t});\n\t\t\tif (page == null) {\n\t\t\t\t// Nothing fetched yet and no page is an empty account, not a failure; after a continuation it\n\t\t\t\t// means the server dropped the drain mid-way.\n\t\t\t\tif (cursor != null) {\n\t\t\t\t\tstopIncomplete('Jira Server returned no page after advertising a continuation');\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tresults.push(\n\t\t\t\t...page.data\n\t\t\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'account' }))\n\t\t\t\t\t.filter((r): r is IssueShape => r != null),\n\t\t\t);\n\t\t\t// An issue the page dropped because it could not be read arrives here, not as a missing row; recorded under\n\t\t\t// the same scope as this read's other failures.\n\t\t\tif (page.metadata != null) {\n\t\t\t\t// Truncated too, as Jira Cloud's read is through `getPagedResult`: the account is missing that issue.\n\t\t\t\ttruncated ||= isIncompleteCollection(page.metadata);\n\t\t\t\tpages.push(withFailureScope(page.metadata, accountScope));\n\t\t\t}\n\n\t\t\tif (!page.hasMore) break;\n\n\t\t\t// More pages are claimed but no advancing cursor came back: the drain can't continue, so flag it\n\t\t\t// rather than stopping silently.\n\t\t\tif (page.nextCursor == null || page.nextCursor === cursor) {\n\t\t\t\tstopIncomplete('Jira Server returned no advancing issue continuation');\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcursor = page.nextCursor;\n\t\t\t// Pages remain but this was the last allowed iteration: the backstop stopped the drain.\n\t\t\tif (i === maxPagesPerRequest - 1) {\n\t\t\t\tstopIncomplete(\n\t\t\t\t\t`Jira Server account-wide issue drain stopped at the ${maxPagesPerRequest}-page backstop`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tvalues: results,\n\t\t\ttruncated: truncated,\n\t\t\tmetadata: mergeCollectionMetadata(summarizePageMetadata(pages), metadata),\n\t\t};\n\t}\n\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: JiraServerResourceDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst issue = await api.getJiraServerIssue(toTokenWithInfo(this.id, session), this.apiBaseUrlFor(session), key);\n\t\treturn issue != null ? toIssueShape(issue, this, { projection: 'point' }) : undefined;\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: JiraServerResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\treturn this.readIssue(session, id, 'point');\n\t}\n\n\tprivate async readIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tid: string,\n\t\tprojection: IssueProjection,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst apiResult = await api.getJiraServerIssue(\n\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\tthis.apiBaseUrlFor(session),\n\t\t\tid,\n\t\t);\n\t\tconst issue = apiResult != null ? toIssueShape(apiResult, this, { projection: projection }) : undefined;\n\t\treturn issue != null ? { ...issue, type: 'issue' } : undefined;\n\t}\n\n\t/**\n\t * The point read behind `getIssuesBatch`'s tracker targets (#5872). `resourceId` is this connection's single\n\t * synthetic resource — the host itself, see {@link JiraServerResourceDescriptor} — so it selects nothing further\n\t * here: the host was chosen upstream by `domain` (or the connection's configured domain) and the request is\n\t * addressed to the SESSION's base URL like every other read. No `resourceUrl` is needed either, unlike Jira\n\t * Cloud: its REST `self` link is an API endpoint, whereas `getJiraServerIssue` builds the browser link from\n\t * the base URL it was addressed to. A key that names no issue resolves to `undefined`, a proven absence.\n\t */\n\tprotected override getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resourceId: string,\n\t\tid: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\treturn this.readIssue(session, id, 'batch');\n\t}\n\n\t/**\n\t * Caches are keyed by the ADDRESS the read was made against as well as the token: one integration instance\n\t * exists per configured host and two hosts can hold the same token, so a token-only key would serve one\n\t * host's projects for the other — and the host alone is not enough either, because a connection re-pointed\n\t * to another context path on the same host keeps its token and would otherwise be served the old path's\n\t * projects.\n\t */\n\tprivate projectsCacheKey(session: ProviderAuthenticationSession): string {\n\t\treturn `${this.apiBaseUrlFor(session)}:${session.accessToken}`;\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._projects.clear();\n\t}\n\n\tprotected override providerOnDisconnect(): void {\n\t\tthis._projects.clear();\n\t}\n}\n"],"mappings":"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"}
@@ -266,29 +266,33 @@ var JiraIntegration = class extends IssuesIntegration {
266
266
  };
267
267
  if (options?.user != null) {
268
268
  const read = await readUserScopedJiraIssues(fetchPage, options.filters, toUserIdentities(options.user, options), drainOptions);
269
- const values = toUniqueJiraIssueShapes(read.issues, this);
269
+ const drain = {
270
+ values: toUniqueJiraIssueShapes(read.issues, this),
271
+ metadata: read.metadata,
272
+ scope: drainOptions.failureScope
273
+ };
270
274
  return read.truncated ? {
271
- values,
275
+ ...drain,
272
276
  truncated: true,
273
- recovery: "none",
274
- metadata: read.metadata
277
+ recovery: "none"
275
278
  } : {
276
- values,
277
- truncated: false,
278
- metadata: read.metadata
279
+ ...drain,
280
+ truncated: false
279
281
  };
280
282
  }
281
283
  const unscoped = await drainJiraIssues(fetchPage, {}, drainOptions);
282
- const values = unscoped.issues.map((issue) => toIssueShape(issue, this, { projection: "project" })).filter((result) => result !== void 0);
284
+ const drain = {
285
+ values: unscoped.issues.map((issue) => toIssueShape(issue, this, { projection: "project" })).filter((result) => result !== void 0),
286
+ metadata: unscoped.metadata,
287
+ scope: drainOptions.failureScope
288
+ };
283
289
  return unscoped.status !== "complete" ? {
284
- values,
290
+ ...drain,
285
291
  truncated: true,
286
- recovery: unscoped.status === "backstop" ? "narrow-scope" : "none",
287
- metadata: unscoped.metadata
292
+ recovery: unscoped.status === "backstop" ? "narrow-scope" : "none"
288
293
  } : {
289
- values,
290
- truncated: false,
291
- metadata: unscoped.metadata
294
+ ...drain,
295
+ truncated: false
292
296
  };
293
297
  }
294
298
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"jira.js","names":[],"sources":["../../../../../plus/integrations/src/providers/jira.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport { JIRA_MAX_PROJECT_KEYS_PER_REQUEST } from '@gitkraken/provider-apis';\nimport * as l10n from '@vscode/l10n';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { AutolinkReference, DynamicAutolinkReference } from '@gitlens/git/models/autolink.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport { mapSettledBounded } from '@gitlens/utils/promise.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport type { ProviderRefusal } from '../collectionMetadata.js';\nimport { toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesCloudHostIntegrationId, providerFanOutConcurrency } from '../constants.js';\nimport type { IssueEtagFields } from '../models/integration.js';\nimport type {\n\tAccountWideIssuesResult,\n\tIssuesForProjectOptions,\n\tProjectIssuesDrain,\n\tProjectIssuesRequest,\n\tSearchMyIssuesOptions,\n} from '../models/issueReads.js';\nimport { groupProjectIssuesSearches, IssuesIntegration } from '../models/issuesIntegration.js';\nimport type { JiraIssueEtagResponse } from './jiraIssueByKey.js';\nimport { jiraBulkFetchMaxKeys, toJiraIssueEtagFields } from './jiraIssueByKey.js';\nimport type { ProviderApiCollectionResult } from './models.js';\nimport { providersMetadata, toAccount, toIssueShape, toProviderIssueStates } from './models.js';\nimport { isJiraMissingProjectError } from './providerErrors.js';\nimport { DiscoveryCache, discoveryCacheTtl } from './utils/discoveryCache.js';\nimport type { JiraDrainOptions, JiraIssuePageFetcher } from './utils/jiraIssueDrains.js';\nimport {\n\tdrainJiraIssues,\n\treadUserScopedJiraIssues,\n\tsearchJiraProjectIssues,\n\tsummarizePageMetadata,\n\ttoUniqueJiraIssueShapes,\n} from './utils/jiraIssueDrains.js';\nimport { collectProviderPagedResult, mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Jira];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n/** A numeric issue id, which Jira's issue reads accept in place of a key. */\nconst numericIssueId = /^\\d+$/;\n\n/**\n * `assignee` and `creator` are user FIELDS: Jira resolves an accountId against the directory, which is the identity\n * that keeps matching once a display name cannot be looked up — a deactivated account, or a profile whose visibility\n * hides the name. Measured against a live site: a deactivated assignee returns its issues by accountId and an empty\n * page by display name, and Jira reports that miss as a successful empty search rather than an error, so the list\n * simply appears empty.\n *\n * `mention` is NOT a user field. It is `comment ~ \"...\"`, a free-text search over comment bodies, so an accountId\n * matches nothing there and the display name is the only value that can. Hence the id is applied to the first two\n * and the handle is kept for the third, rather than swapping `user` wholesale.\n */\nfunction toUserIdentities(user: string, options: IssuesForProjectOptions): { userField: string; mention: string } {\n\treturn { userField: options.userId ?? user, mention: user };\n}\n\nexport type JiraBaseDescriptor = IssueResourceDescriptor;\n\nexport interface JiraOrganizationDescriptor extends JiraBaseDescriptor {\n\turl: string;\n\tavatarUrl: string;\n}\n\nexport interface JiraProjectDescriptor extends JiraBaseDescriptor {\n\tresourceId: string;\n}\n\nexport class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Jira> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\treadonly id = IssuesCloudHostIntegrationId.Jira;\n\tprotected readonly key = this.id;\n\treadonly name: string = 'Jira';\n\n\tget domain(): string {\n\t\treturn metadata.domain;\n\t}\n\n\tprotected get apiBaseUrl(): string {\n\t\treturn 'https://api.atlassian.com';\n\t}\n\n\t/**\n\t * Built from the sites and their projects, so it follows them: rebuilt when a site's project list changes or the\n\t * caches are dropped, and once older than {@link discoveryCacheTtl}. An expired set is still served while it is\n\t * rebuilt in the background, because autolinks are read on every render of a commit message and must not wait\n\t * on the network, nor go empty just because nothing read the projects for a while (#5907).\n\t */\n\tprivate readonly _autolinks = new Map<\n\t\tstring,\n\t\t{ autolinks: (AutolinkReference | DynamicAutolinkReference)[]; builtAt: number }\n\t>();\n\tprivate readonly _autolinksBuilds = new Map<string, Promise<(AutolinkReference | DynamicAutolinkReference)[]>>();\n\n\toverride async autolinks(): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected || this._session == null) return [];\n\n\t\tconst session = this._session;\n\t\tconst cached = this._autolinks.get(session.accessToken);\n\t\t// A session kept through a failed refresh is expired: building with it would send that token to the provider\n\t\t// on every render, so serve whatever was built before (none after a forced re-sync) until a read refreshes it.\n\t\tif (this.connectionExpired === true) return cached?.autolinks ?? [];\n\t\tif (cached == null) return this.buildAutolinks(session);\n\n\t\tif (Date.now() - cached.builtAt >= discoveryCacheTtl) {\n\t\t\tvoid this.buildAutolinks(session);\n\t\t}\n\t\treturn cached.autolinks;\n\t}\n\n\t/** One build per token at a time: concurrent renders share it rather than each re-reading sites and projects. */\n\tprivate buildAutolinks(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst { accessToken } = session;\n\t\tlet build = this._autolinksBuilds.get(accessToken);\n\t\tif (build == null) {\n\t\t\tbuild = this.buildAutolinksCore(session).finally(() => this._autolinksBuilds.delete(accessToken));\n\t\t\tthis._autolinksBuilds.set(accessToken, build);\n\t\t}\n\t\treturn build;\n\t}\n\n\tprivate async buildAutolinksCore(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst { accessToken } = session;\n\t\tconst generation = this._projects.generation;\n\t\ttry {\n\t\t\tconst organizations = await this.getProviderResourcesForUser(session);\n\t\t\tif (!organizations?.length) return [];\n\n\t\t\t// Served from the cache for every site whose projects are still fresh; only the rest are read.\n\t\t\tconst { values: projects, metadata } = await this.getProviderProjectsForResourcesWithMetadata(\n\t\t\t\tsession,\n\t\t\t\torganizations,\n\t\t\t);\n\n\t\t\tconst autolinks: (AutolinkReference | DynamicAutolinkReference)[] = [];\n\t\t\tfor (const project of projects) {\n\t\t\t\tconst organization = organizations.find(o => o.id === project.resourceId);\n\t\t\t\tif (organization == null) continue;\n\n\t\t\t\tconst dashedPrefix = `${project.key}-`;\n\t\t\t\tconst underscoredPrefix = `${project.key}_`;\n\t\t\t\tautolinks.push({\n\t\t\t\t\tprefix: dashedPrefix,\n\t\t\t\t\turl: `${organization.url}/browse/${dashedPrefix}<num>`,\n\t\t\t\t\talphanumeric: false,\n\t\t\t\t\tignoreCase: false,\n\t\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\t\ttype: 'issue',\n\t\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\t\tdescriptor: { ...organization },\n\t\t\t\t});\n\t\t\t\tautolinks.push({\n\t\t\t\t\tprefix: underscoredPrefix,\n\t\t\t\t\turl: `${organization.url}/browse/${dashedPrefix}<num>`,\n\t\t\t\t\talphanumeric: false,\n\t\t\t\t\tignoreCase: false,\n\t\t\t\t\treferenceType: 'branch',\n\t\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\t\ttype: 'issue',\n\t\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\t\tdescriptor: { ...organization },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// A set missing a site's projects is served but not kept, so the next call tries that site again; one\n\t\t\t// built from projects read before the caches were dropped is not kept either.\n\t\t\tconst complete = metadata == null || metadata.completeness === 'complete';\n\t\t\tif (complete && this._projects.generation === generation) {\n\t\t\t\tthis._autolinks.set(accessToken, { autolinks: autolinks, builtAt: Date.now() });\n\t\t\t}\n\t\t\treturn autolinks;\n\t\t} catch (ex) {\n\t\t\t// Autolinks are decoration: keep serving the last set rather than failing the render that asked.\n\t\t\tLogger.error(ex, 'JiraIntegration.autolinks');\n\t\t\treturn this._autolinks.get(accessToken)?.autolinks ?? [];\n\t\t}\n\t}\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: JiraOrganizationDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getCurrentUserForResource(toTokenWithInfo(this.id, session), resource.id);\n\n\t\tif (user == null) return undefined;\n\t\treturn toAccount(user, this);\n\t}\n\n\t/** Atlassian's account id is the same on every site, so the first site answers for all of them. */\n\tprotected override async getProviderCurrentAccount(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<Account | undefined> {\n\t\tconst resource = (await this.getProviderResourcesForUser(session))?.[0];\n\t\treturn resource != null ? this.getProviderAccountForResource(session, resource) : undefined;\n\t}\n\n\t/** The site-list request the resource cache would otherwise answer; see `IntegrationBase.validateCredential`. */\n\tprotected override async validateCredential(session: ProviderAuthenticationSession): Promise<void> {\n\t\tconst api = await this.getProvidersApi();\n\t\tif ((await api.getJiraResourcesForCurrentUser(toTokenWithInfo(this.id, session))) == null) {\n\t\t\tthrow new Error('Jira did not confirm the credential');\n\t\t}\n\t}\n\n\t/**\n\t * Atlassian's `401` for a token missing a scope the request needs, which a reconnect fixes by consenting to it\n\t * again; see `IntegrationBase.isCredentialRefusal`. Atlassian gives the same answer to a malformed request URL,\n\t * so a request the SDK built wrong would read as the credential's too.\n\t */\n\tprotected override isCredentialRefusal(refusal: ProviderRefusal): boolean {\n\t\treturn refusal.status === 401 && /scope does not match/i.test(refusal.detail ?? '');\n\t}\n\n\t/**\n\t * The sites and the projects discovered under them, both of which expire and are dropped on a re-sync, so a site\n\t * or project created or deleted mid-session is picked up (#5907). Keyed by token, and projects by token and site.\n\t */\n\tprivate readonly _organizations = new DiscoveryCache<JiraOrganizationDescriptor[]>();\n\tprivate readonly _projects = new DiscoveryCache<JiraProjectDescriptor[]>();\n\t/** When the caches were last dropped; a persisted discovery read before then is as stale as they were. */\n\tprivate _discoveryInvalidatedAt = 0;\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<JiraOrganizationDescriptor[] | undefined> {\n\t\tconst { accessToken } = session;\n\n\t\tconst cachedResources = this._organizations.get(accessToken);\n\t\tif (cachedResources != null && !force) return cachedResources;\n\n\t\tconst generation = this._organizations.generation;\n\t\tconst api = await this.getProvidersApi();\n\t\tconst resources = await api.getJiraResourcesForCurrentUser(toTokenWithInfo(this.id, session));\n\t\tconst organizations = resources?.map(r => ({ ...r, key: r.id }));\n\t\tif (organizations == null) {\n\t\t\tthis._organizations.delete(accessToken);\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis._organizations.set(accessToken, organizations, { generation: generation });\n\t\treturn organizations;\n\t}\n\n\tprotected override async getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraOrganizationDescriptor[],\n\t\tforce: boolean = false,\n\t): Promise<JiraProjectDescriptor[] | undefined> {\n\t\treturn (await this.getProviderProjectsForResourcesWithMetadata(session, resources, force)).values;\n\t}\n\n\tprotected override async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraOrganizationDescriptor[],\n\t\tforce: boolean = false,\n\t): Promise<ProviderApiCollectionResult<JiraProjectDescriptor>> {\n\t\tconst { accessToken } = session;\n\t\tconst projectsCache = this._projects;\n\t\tconst generation = projectsCache.generation;\n\n\t\tlet resourcesWithoutProjects = [];\n\t\tif (force) {\n\t\t\tresourcesWithoutProjects = resources;\n\t\t} else {\n\t\t\tfor (const resource of resources) {\n\t\t\t\tconst resourceKey = `${accessToken}:${resource.id}`;\n\t\t\t\tconst cachedProjects = projectsCache.get(resourceKey);\n\t\t\t\tif (cachedProjects == null) {\n\t\t\t\t\tresourcesWithoutProjects.push(resource);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\tconst partialProjects: JiraProjectDescriptor[] = [];\n\t\tif (resourcesWithoutProjects.length > 0) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\t\tconst drains = await Promise.allSettled(\n\t\t\t\tresourcesWithoutProjects.map(async resource => ({\n\t\t\t\t\tresource: resource,\n\t\t\t\t\tresult: await collectProviderPagedResult(\n\t\t\t\t\t\tcursor => api.getJiraProjectsForResource(tokenWithInfo, resource.id, { cursor: cursor }),\n\t\t\t\t\t\tmaxPagesPerRequest,\n\t\t\t\t\t\t{ providerId: this.id, resourceId: resource.id },\n\t\t\t\t\t),\n\t\t\t\t})),\n\t\t\t);\n\n\t\t\tdrains.forEach((drain, index) => {\n\t\t\t\tif (drain.status === 'rejected') {\n\t\t\t\t\tconst resource = resourcesWithoutProjects[index];\n\t\t\t\t\tif (resource == null) return;\n\n\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\t\tfailures: [\n\t\t\t\t\t\t\ttoCollectionScopeFailure({ providerId: this.id, resourceId: resource.id }, drain.reason),\n\t\t\t\t\t\t],\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst { resource, result } = drain.value;\n\t\t\t\tmetadata = mergeCollectionMetadata(metadata, result.metadata);\n\t\t\t\tconst projects = result.values\n\t\t\t\t\t.filter(project => project.resourceId === resource.id)\n\t\t\t\t\t.map(project => ({ ...project }));\n\t\t\t\tconst incomplete =\n\t\t\t\t\tresult.truncated === true ||\n\t\t\t\t\t(result.metadata != null && result.metadata.completeness !== 'complete');\n\t\t\t\tif (incomplete) {\n\t\t\t\t\tpartialProjects.push(...projects);\n\t\t\t\t\tif (\n\t\t\t\t\t\tresult.truncated === true &&\n\t\t\t\t\t\t(result.metadata == null || result.metadata.completeness === 'complete')\n\t\t\t\t\t) {\n\t\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, { completeness: 'partial' });\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Not stored when the cache was dropped while this read was in flight, but still returned below: it\n\t\t\t\t// is what this read found, only no longer trusted for the next one.\n\t\t\t\tif (!projectsCache.set(`${accessToken}:${resource.id}`, projects, { generation: generation })) {\n\t\t\t\t\tpartialProjects.push(...projects);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tthis._autolinks.delete(accessToken);\n\t\t\t});\n\t\t}\n\n\t\tconst values = resources.reduce<JiraProjectDescriptor[]>((projects, resource) => {\n\t\t\tconst resourceProjects = projectsCache.get(`${accessToken}:${resource.id}`);\n\t\t\tif (resourceProjects != null) {\n\t\t\t\tprojects.push(...resourceProjects);\n\t\t\t}\n\t\t\treturn projects;\n\t\t}, partialProjects);\n\n\t\tconst projectsByIdentity = new Map<string, JiraProjectDescriptor>();\n\t\tfor (const project of values) {\n\t\t\tconst identity = `${project.resourceId}:${project.id}`;\n\t\t\tif (!projectsByIdentity.has(identity)) {\n\t\t\t\tprojectsByIdentity.set(identity, project);\n\t\t\t}\n\t\t}\n\t\treturn { values: [...projectsByIdentity.values()], metadata: metadata };\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\tconst api = await this.getProvidersApi();\n\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\t// By key, not name, as the multi-project search: JQL's `project = \"<value>\"` also accepts a name or id, so a\n\t\t// display name equal to another project's key would address that project. The key is the only unambiguous one.\n\t\tconst fetchPage: JiraIssuePageFetcher = (scope, cursor) =>\n\t\t\tapi.getIssuesForProjectPaged(tokenWithInfo, project.key, project.resourceId, {\n\t\t\t\t...scope,\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t\tsort: options?.sort,\n\t\t\t});\n\t\tconst drainOptions: JiraDrainOptions = {\n\t\t\tmaxPages: maxPagesPerRequest,\n\t\t\tfailureScope: { providerId: this.id, resourceId: project.resourceId, projectId: project.key },\n\t\t\tproviderName: 'Jira',\n\t\t\t// A project this token can no longer see (deleted, or its browse permission removed) holds no issues\n\t\t\t// for it, and the site's project list that named it is stale: drop that list so the next read\n\t\t\t// re-reads it, instead of failing the whole provider on every read until a restart (#5907).\n\t\t\tisEmptyFailure: ex => {\n\t\t\t\tif (!isJiraMissingProjectError(ex)) return false;\n\n\t\t\t\tLogger.warn(`Jira project '${project.key}' no longer exists or is not visible`);\n\t\t\t\tthis._projects.delete(`${session.accessToken}:${project.resourceId}`);\n\t\t\t\tthis._autolinks.delete(session.accessToken);\n\t\t\t\treturn true;\n\t\t\t},\n\t\t};\n\n\t\tif (options?.user != null) {\n\t\t\tconst read = await readUserScopedJiraIssues(\n\t\t\t\tfetchPage,\n\t\t\t\toptions.filters,\n\t\t\t\ttoUserIdentities(options.user, options),\n\t\t\t\tdrainOptions,\n\t\t\t);\n\t\t\tconst values = toUniqueJiraIssueShapes(read.issues, this);\n\t\t\treturn read.truncated\n\t\t\t\t? { values: values, truncated: true, recovery: 'none', metadata: read.metadata }\n\t\t\t\t: { values: values, truncated: false, metadata: read.metadata };\n\t\t}\n\n\t\tconst unscoped = await drainJiraIssues(fetchPage, {}, drainOptions);\n\t\tconst values = unscoped.issues\n\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'project' }))\n\t\t\t.filter((result): result is IssueShape => result !== undefined);\n\t\treturn unscoped.status !== 'complete'\n\t\t\t? {\n\t\t\t\t\tvalues: values,\n\t\t\t\t\ttruncated: true,\n\t\t\t\t\trecovery: unscoped.status === 'backstop' ? 'narrow-scope' : 'none',\n\t\t\t\t\tmetadata: unscoped.metadata,\n\t\t\t\t}\n\t\t\t: { values: values, truncated: false, metadata: unscoped.metadata };\n\t}\n\n\t/**\n\t * Searches a site's projects together for a user-scoped read: one JQL names up to\n\t * {@link JIRA_MAX_PROJECT_KEYS_PER_REQUEST} projects of one site and one user scope, past which the SDK refuses\n\t * the call rather than risk a query string Jira rejects.\n\t *\n\t * An unscoped read is never searched across projects: it drains every issue in the project, and its backstop is\n\t * reported as `narrow-scope` per project, which one search across projects could not attribute.\n\t */\n\tprotected override getProjectIssuesSearches(\n\t\trequests: readonly ProjectIssuesRequest<JiraProjectDescriptor>[],\n\t): number[][] | undefined {\n\t\treturn groupProjectIssuesSearches(\n\t\t\trequests,\n\t\t\t({ project, options }) =>\n\t\t\t\toptions.user != null\n\t\t\t\t\t? [\n\t\t\t\t\t\t\tproject.resourceId,\n\t\t\t\t\t\t\toptions.user,\n\t\t\t\t\t\t\toptions.userId ?? '',\n\t\t\t\t\t\t\toptions.sort ?? '',\n\t\t\t\t\t\t\toptions.state ?? '',\n\t\t\t\t\t\t\toptions.filters?.join(',') ?? '',\n\t\t\t\t\t\t].join('\\0')\n\t\t\t\t\t: undefined,\n\t\t\tJIRA_MAX_PROJECT_KEYS_PER_REQUEST,\n\t\t);\n\t}\n\n\tprotected override async searchProviderProjectIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\trequests: readonly ProjectIssuesRequest<JiraProjectDescriptor>[],\n\t): Promise<IssueShape[][] | undefined> {\n\t\t// Every request of a search shares its site and user scope (see `getProjectIssuesSearches`).\n\t\tconst { project, options } = requests[0];\n\t\tif (options.user == null) return undefined;\n\t\t// One project's search is its own read, by the same key and user clauses, so it costs no more to read it as one;\n\t\t// and that read attributes a failure to the project and drops a project list that no longer has it.\n\t\tif (requests.length === 1) return undefined;\n\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst api = await this.getProvidersApi();\n\t\t// By key, not name: JQL resolves `project = \"<value>\"` against key first, so a name could read another\n\t\t// project's issues.\n\t\tconst projectKeys = requests.map(request => request.project.key);\n\t\tconst states = toProviderIssueStates(options.state);\n\n\t\treturn searchJiraProjectIssues(\n\t\t\t(userScope, cursor) =>\n\t\t\t\tapi.getIssuesForProjectsPaged(tokenWithInfo, projectKeys, project.resourceId, {\n\t\t\t\t\t...userScope,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tstates: states,\n\t\t\t\t\tsort: options.sort,\n\t\t\t\t}),\n\t\t\trequests.map(request => request.project.id),\n\t\t\toptions.filters,\n\t\t\ttoUserIdentities(options.user, options),\n\t\t\t{\n\t\t\t\tmaxPages: maxPagesPerRequest,\n\t\t\t\tfailureScope: { providerId: this.id, resourceId: project.resourceId },\n\t\t\t\tproviderName: 'Jira',\n\t\t\t},\n\t\t\tthis,\n\t\t);\n\t}\n\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: JiraOrganizationDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\tconst result = await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation);\n\t\t// This read can only return values, so what the account-wide read reports as missing is at least logged.\n\t\tfor (const failure of result?.metadata?.failures ?? []) {\n\t\t\tLogger.warn(\n\t\t\t\t`searchProviderMyIssues: Jira site '${failure.scope?.resourceId ?? '?'}' was not fully read: ${failure.message ?? failure.kind}`,\n\t\t\t);\n\t\t}\n\t\treturn result?.values;\n\t}\n\n\t/**\n\t * Overridden so `state` reaches the per-site read, which the default drops. Each site is drained on its own, so a\n\t * site that fails keeps what it read and is recorded as that site's failure instead of discarding the sites that\n\t * answered; so is an issue a page dropped because it could not be read. A page backstop or a stalled continuation\n\t * makes the read `truncated`. A refused site therefore goes through `confirmScopedAuthFailures`, which publishes it\n\t * as that site's auth warning or, if the credential itself is refused, as the connection's.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: JiraOrganizationDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tconst myResources = resources ?? (await this.getProviderResourcesForUser(session));\n\t\tif (!myResources) return undefined;\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\tconst results: IssueShape[] = [];\n\t\tlet truncated = false;\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\t// What each page reported failing, summarized at the end so a site whose many pages dropped issues reports\n\t\t// it once (see `summarizePageMetadata`); the pages keep the rest of their metadata, so `truncated` holds.\n\t\tconst pages: CollectionMetadata[] = [];\n\t\tconst readPage = async (resourceId: string, cursor: string | undefined) => {\n\t\t\tconst page = await api.getIssuesForResourceForCurrentUser(tokenWithInfo, resourceId, {\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t});\n\t\t\tif (!page.metadata?.failures?.length) return page;\n\n\t\t\tconst { failures: _failures, ...rest } = page.metadata;\n\t\t\tpages.push(page.metadata);\n\t\t\treturn { ...page, metadata: rest };\n\t\t};\n\t\tfor (const resource of myResources) {\n\t\t\tif (cancellation?.aborted) break;\n\n\t\t\tconst site = await collectProviderPagedResult(\n\t\t\t\tcursor =>\n\t\t\t\t\t// Cancellation is not truncation: the caller asked for the stop, so the site ends where it is.\n\t\t\t\t\tcancellation?.aborted\n\t\t\t\t\t\t? Promise.resolve({ values: [], paging: { cursor: '{}', more: false } })\n\t\t\t\t\t\t: readPage(resource.id, cursor),\n\t\t\t\tmaxPagesPerRequest,\n\t\t\t\t{ providerId: this.id, resourceId: resource.id },\n\t\t\t);\n\t\t\tresults.push(\n\t\t\t\t...site.values\n\t\t\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'account' }))\n\t\t\t\t\t.filter((result): result is IssueShape => result != null),\n\t\t\t);\n\t\t\ttruncated ||= site.truncated === true;\n\t\t\tmetadata = mergeCollectionMetadata(metadata, site.metadata);\n\t\t}\n\n\t\treturn {\n\t\t\tvalues: results,\n\t\t\ttruncated: truncated,\n\t\t\tmetadata: mergeCollectionMetadata(summarizePageMetadata(pages), metadata),\n\t\t};\n\t}\n\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: JiraOrganizationDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst issue = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: key,\n\t\t});\n\t\treturn issue != null ? toIssueShape(issue, this, { projection: 'point' }) : undefined;\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: JiraOrganizationDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst apiResult = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: id,\n\t\t});\n\t\tconst issue = apiResult != null ? toIssueShape(apiResult, this, { projection: 'point' }) : undefined;\n\t\treturn issue != null ? { ...issue, type: 'issue' } : undefined;\n\t}\n\n\tprotected override async getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresourceId: string,\n\t\tid: string,\n\t\tresourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\tif (resourceUrl == null) {\n\t\t\tthrow new Error('Jira direct issue reads require a resource URL');\n\t\t}\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst apiResult = await api.getJiraIssueByKey(toTokenWithInfo(this.id, session), resourceId, resourceUrl, id);\n\t\tconst issue = apiResult != null ? toIssueShape(apiResult, this, { projection: 'batch' }) : undefined;\n\t\treturn issue != null ? { ...issue, type: 'issue' } : undefined;\n\t}\n\n\t/**\n\t * The cheap check behind the batch issue read's etags: one bulk fetch per site and {@link jiraBulkFetchMaxKeys}\n\t * distinct keys, where {@link getProviderIssueByResourceId} sends one request per issue. A request that throws\n\t * rejects only its own targets' slots, with its error classified as the full read's would be.\n\t *\n\t * A target identified by a numeric issue id, which both reads accept as well as a key, is matched by the returned\n\t * issue's `id`; every other target by its key.\n\t *\n\t * Never proves an absence. Jira silently omits a key it can't find or show (listing it in no `issueErrors` either),\n\t * and answers a moved issue under its new key, which no longer matches the key asked for. Either way the target's\n\t * slot is rejected, so the full read, whose 404 does prove an absence, decides. When no target was answered and\n\t * nothing failed, the check declines instead, so a batch of only such keys costs a full read, not a failure.\n\t */\n\tprotected override async getProviderIssuesEtagFieldsByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\ttargets: readonly { resourceId: string; identifier: string; resourceUrl?: string }[],\n\t): Promise<PromiseSettledResult<IssueEtagFields | undefined>[] | undefined> {\n\t\tconst slots = new Array<PromiseSettledResult<IssueEtagFields | undefined>>(targets.length);\n\t\tlet answered = false;\n\t\tlet failed = false;\n\n\t\t// Each site's distinct keys, matched case-insensitively as Jira matches them, with the targets asking for each.\n\t\tconst bySite = new Map<string, Map<string, number[]>>();\n\t\tfor (const [index, target] of targets.entries()) {\n\t\t\tif (target.resourceUrl == null) {\n\t\t\t\t// Required as the full read requires it, so a check never answers for a target the full read refuses.\n\t\t\t\tslots[index] = {\n\t\t\t\t\tstatus: 'rejected',\n\t\t\t\t\treason: new Error('Jira direct issue reads require a resource URL'),\n\t\t\t\t};\n\t\t\t\tfailed = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet keys = bySite.get(target.resourceId);\n\t\t\tif (keys == null) {\n\t\t\t\tkeys = new Map();\n\t\t\t\tbySite.set(target.resourceId, keys);\n\t\t\t}\n\n\t\t\tconst key = target.identifier.toUpperCase();\n\t\t\tlet indices = keys.get(key);\n\t\t\tif (indices == null) {\n\t\t\t\tindices = [];\n\t\t\t\tkeys.set(key, indices);\n\t\t\t}\n\t\t\tindices.push(index);\n\t\t}\n\n\t\tconst requests = [...bySite].flatMap(([resourceId, keys]) =>\n\t\t\tchunk([...keys], jiraBulkFetchMaxKeys).map(entries => ({ resourceId: resourceId, entries: entries })),\n\t\t);\n\t\tif (!requests.length) return slots;\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst answers = await mapSettledBounded(requests, providerFanOutConcurrency, r =>\n\t\t\tapi.getJiraIssuesEtagFields(\n\t\t\t\ttokenWithInfo,\n\t\t\t\tr.resourceId,\n\t\t\t\tr.entries.map(([key]) => key),\n\t\t\t),\n\t\t);\n\n\t\tfor (const [i, answer] of answers.entries()) {\n\t\t\tconst { entries } = requests[i];\n\t\t\tif (answer.status === 'rejected') {\n\t\t\t\tfailed = true;\n\t\t\t\tfor (const [, indices] of entries) {\n\t\t\t\t\tfor (const index of indices) {\n\t\t\t\t\t\tslots[index] = answer;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (answer.value.errorCount > 0) {\n\t\t\t\tfailed = true;\n\t\t\t}\n\n\t\t\t// By key or id, never by position: Jira returns the issues in its own order, and only the ones it found.\n\t\t\tconst byKey = new Map<string, JiraIssueEtagResponse>();\n\t\t\tconst byId = new Map<string, JiraIssueEtagResponse>();\n\t\t\tfor (const issue of answer.value.issues) {\n\t\t\t\tbyKey.set(issue.key.toUpperCase(), issue);\n\t\t\t\tbyId.set(issue.id, issue);\n\t\t\t}\n\n\t\t\tfor (const [key, indices] of entries) {\n\t\t\t\t// A key target never matches by id, so an issue returned under a different key (moved) is rejected.\n\t\t\t\tconst issue = numericIssueId.test(key) ? byId.get(key) : byKey.get(key);\n\t\t\t\tlet slot: PromiseSettledResult<IssueEtagFields | undefined>;\n\t\t\t\tif (issue == null) {\n\t\t\t\t\tslot = { status: 'rejected', reason: new Error(`Jira bulk fetch did not return ${key}`) };\n\t\t\t\t} else {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tslot = { status: 'fulfilled', value: toJiraIssueEtagFields(issue) };\n\t\t\t\t\t\tanswered = true;\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\tslot = { status: 'rejected', reason: ex };\n\t\t\t\t\t\tfailed = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor (const index of indices) {\n\t\t\t\t\tslots[index] = slot;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn answered || failed ? slots : undefined;\n\t}\n\n\tprotected override async providerOnConnect(): Promise<void> {\n\t\tthis._autolinks.clear();\n\t\tif (this._session == null) return;\n\n\t\tconst session = this._session;\n\t\tconst { accessToken } = session;\n\t\tconst organizationsGeneration = this._organizations.generation;\n\t\tconst projectsGeneration = this._projects.generation;\n\n\t\t// Persisted discovery seeds the caches only while it would still be fresh in them: read within the TTL, and\n\t\t// after the caches were last dropped. Otherwise a re-sync, which runs this again, would put back the very\n\t\t// project list it just dropped, and a restart would trust one read any time before (#5907).\n\t\tconst isFresh = (timestamp: unknown): timestamp is number =>\n\t\t\ttypeof timestamp === 'number' &&\n\t\t\ttimestamp > this._discoveryInvalidatedAt &&\n\t\t\tDate.now() - timestamp < discoveryCacheTtl;\n\n\t\tconst storedOrganizations = this.ctx.storage.get(`jira:${accessToken}:organizations`);\n\t\t// Projects persisted without their sites belong to a session whose sites were cleared below; ignore them.\n\t\tconst storedProjects =\n\t\t\tstoredOrganizations != null ? this.ctx.storage.get(`jira:${accessToken}:projects`) : undefined;\n\n\t\tlet organizations: JiraOrganizationDescriptor[] | undefined;\n\t\tif (storedOrganizations?.data != null && isFresh(storedOrganizations.timestamp)) {\n\t\t\tconst seeded = storedOrganizations.data.map((o: JiraOrganizationDescriptor) => ({ ...o }));\n\t\t\torganizations = seeded;\n\t\t\tthis._organizations.set(accessToken, seeded, {\n\t\t\t\tgeneration: organizationsGeneration,\n\t\t\t\tstoredAt: storedOrganizations.timestamp,\n\t\t\t});\n\t\t} else {\n\t\t\t// Stamped with when the read started, so the TTL counts from what it actually saw.\n\t\t\tconst readAt = Date.now();\n\t\t\torganizations = await this.getProviderResourcesForUser(session, true);\n\t\t\tif (storedOrganizations == null) {\n\t\t\t\t// Clear all other stored organizations and projects when our session changes\n\t\t\t\tawait this.ctx.storage.deleteWithPrefix('jira');\n\t\t\t}\n\t\t\t// Not persisted when the caches were dropped while it was in flight: it is then a read from before the\n\t\t\t// refresh, and the next start, which knows nothing of that refresh, would take it for fresh.\n\t\t\tif (this._organizations.generation === organizationsGeneration) {\n\t\t\t\tawait this.ctx.storage.store(`jira:${accessToken}:organizations`, {\n\t\t\t\t\tv: 1,\n\t\t\t\t\ttimestamp: readAt,\n\t\t\t\t\tdata: organizations,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tif (storedProjects?.data != null && isFresh(storedProjects.timestamp)) {\n\t\t\tconst projectsByResource = new Map<string, JiraProjectDescriptor[]>();\n\t\t\tfor (const project of storedProjects.data as JiraProjectDescriptor[]) {\n\t\t\t\tconst projects = projectsByResource.get(project.resourceId);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tprojectsByResource.set(project.resourceId, [{ ...project }]);\n\t\t\t\t} else if (!projects.some(p => p.id === project.id)) {\n\t\t\t\t\tprojects.push({ ...project });\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const [resourceId, projects] of projectsByResource) {\n\t\t\t\tthis._projects.set(`${accessToken}:${resourceId}`, projects, {\n\t\t\t\t\tgeneration: projectsGeneration,\n\t\t\t\t\tstoredAt: storedProjects.timestamp,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (organizations?.length) {\n\t\t\tconst readAt = Date.now();\n\t\t\tconst projects = await this.getProviderProjectsForResources(session, organizations, true);\n\t\t\t// See the organizations above.\n\t\t\tif (this._projects.generation === projectsGeneration) {\n\t\t\t\tawait this.ctx.storage.store(`jira:${accessToken}:projects`, {\n\t\t\t\t\tv: 1,\n\t\t\t\t\ttimestamp: readAt,\n\t\t\t\t\tdata: projects,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._discoveryInvalidatedAt = Date.now();\n\t\tthis._organizations.clear();\n\t\tthis._projects.clear();\n\t\tthis._autolinks.clear();\n\t}\n\n\tprotected override providerOnDisconnect(): void {\n\t\tthis._organizations.clear();\n\t\tthis._projects.clear();\n\t\tthis._autolinks.clear();\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"jira.js","names":[],"sources":["../../../../../plus/integrations/src/providers/jira.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport { JIRA_MAX_PROJECT_KEYS_PER_REQUEST } from '@gitkraken/provider-apis';\nimport * as l10n from '@vscode/l10n';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { AutolinkReference, DynamicAutolinkReference } from '@gitlens/git/models/autolink.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport { mapSettledBounded } from '@gitlens/utils/promise.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport type { ProviderRefusal } from '../collectionMetadata.js';\nimport { toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesCloudHostIntegrationId, providerFanOutConcurrency } from '../constants.js';\nimport type { IssueEtagFields } from '../models/integration.js';\nimport type {\n\tAccountWideIssuesResult,\n\tIssuesForProjectOptions,\n\tProjectIssuesDrain,\n\tProjectIssuesRequest,\n\tSearchMyIssuesOptions,\n} from '../models/issueReads.js';\nimport { groupProjectIssuesSearches, IssuesIntegration } from '../models/issuesIntegration.js';\nimport type { JiraIssueEtagResponse } from './jiraIssueByKey.js';\nimport { jiraBulkFetchMaxKeys, toJiraIssueEtagFields } from './jiraIssueByKey.js';\nimport type { ProviderApiCollectionResult } from './models.js';\nimport { providersMetadata, toAccount, toIssueShape, toProviderIssueStates } from './models.js';\nimport { isJiraMissingProjectError } from './providerErrors.js';\nimport { DiscoveryCache, discoveryCacheTtl } from './utils/discoveryCache.js';\nimport type { JiraDrainOptions, JiraIssuePageFetcher } from './utils/jiraIssueDrains.js';\nimport {\n\tdrainJiraIssues,\n\treadUserScopedJiraIssues,\n\tsearchJiraProjectIssues,\n\tsummarizePageMetadata,\n\ttoUniqueJiraIssueShapes,\n} from './utils/jiraIssueDrains.js';\nimport { collectProviderPagedResult, mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Jira];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n/** A numeric issue id, which Jira's issue reads accept in place of a key. */\nconst numericIssueId = /^\\d+$/;\n\n/**\n * `assignee` and `creator` are user FIELDS: Jira resolves an accountId against the directory, which is the identity\n * that keeps matching once a display name cannot be looked up — a deactivated account, or a profile whose visibility\n * hides the name. Measured against a live site: a deactivated assignee returns its issues by accountId and an empty\n * page by display name, and Jira reports that miss as a successful empty search rather than an error, so the list\n * simply appears empty.\n *\n * `mention` is NOT a user field. It is `comment ~ \"...\"`, a free-text search over comment bodies, so an accountId\n * matches nothing there and the display name is the only value that can. Hence the id is applied to the first two\n * and the handle is kept for the third, rather than swapping `user` wholesale.\n */\nfunction toUserIdentities(user: string, options: IssuesForProjectOptions): { userField: string; mention: string } {\n\treturn { userField: options.userId ?? user, mention: user };\n}\n\nexport type JiraBaseDescriptor = IssueResourceDescriptor;\n\nexport interface JiraOrganizationDescriptor extends JiraBaseDescriptor {\n\turl: string;\n\tavatarUrl: string;\n}\n\nexport interface JiraProjectDescriptor extends JiraBaseDescriptor {\n\tresourceId: string;\n}\n\nexport class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Jira> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\treadonly id = IssuesCloudHostIntegrationId.Jira;\n\tprotected readonly key = this.id;\n\treadonly name: string = 'Jira';\n\n\tget domain(): string {\n\t\treturn metadata.domain;\n\t}\n\n\tprotected get apiBaseUrl(): string {\n\t\treturn 'https://api.atlassian.com';\n\t}\n\n\t/**\n\t * Built from the sites and their projects, so it follows them: rebuilt when a site's project list changes or the\n\t * caches are dropped, and once older than {@link discoveryCacheTtl}. An expired set is still served while it is\n\t * rebuilt in the background, because autolinks are read on every render of a commit message and must not wait\n\t * on the network, nor go empty just because nothing read the projects for a while (#5907).\n\t */\n\tprivate readonly _autolinks = new Map<\n\t\tstring,\n\t\t{ autolinks: (AutolinkReference | DynamicAutolinkReference)[]; builtAt: number }\n\t>();\n\tprivate readonly _autolinksBuilds = new Map<string, Promise<(AutolinkReference | DynamicAutolinkReference)[]>>();\n\n\toverride async autolinks(): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected || this._session == null) return [];\n\n\t\tconst session = this._session;\n\t\tconst cached = this._autolinks.get(session.accessToken);\n\t\t// A session kept through a failed refresh is expired: building with it would send that token to the provider\n\t\t// on every render, so serve whatever was built before (none after a forced re-sync) until a read refreshes it.\n\t\tif (this.connectionExpired === true) return cached?.autolinks ?? [];\n\t\tif (cached == null) return this.buildAutolinks(session);\n\n\t\tif (Date.now() - cached.builtAt >= discoveryCacheTtl) {\n\t\t\tvoid this.buildAutolinks(session);\n\t\t}\n\t\treturn cached.autolinks;\n\t}\n\n\t/** One build per token at a time: concurrent renders share it rather than each re-reading sites and projects. */\n\tprivate buildAutolinks(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst { accessToken } = session;\n\t\tlet build = this._autolinksBuilds.get(accessToken);\n\t\tif (build == null) {\n\t\t\tbuild = this.buildAutolinksCore(session).finally(() => this._autolinksBuilds.delete(accessToken));\n\t\t\tthis._autolinksBuilds.set(accessToken, build);\n\t\t}\n\t\treturn build;\n\t}\n\n\tprivate async buildAutolinksCore(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst { accessToken } = session;\n\t\tconst generation = this._projects.generation;\n\t\ttry {\n\t\t\tconst organizations = await this.getProviderResourcesForUser(session);\n\t\t\tif (!organizations?.length) return [];\n\n\t\t\t// Served from the cache for every site whose projects are still fresh; only the rest are read.\n\t\t\tconst { values: projects, metadata } = await this.getProviderProjectsForResourcesWithMetadata(\n\t\t\t\tsession,\n\t\t\t\torganizations,\n\t\t\t);\n\n\t\t\tconst autolinks: (AutolinkReference | DynamicAutolinkReference)[] = [];\n\t\t\tfor (const project of projects) {\n\t\t\t\tconst organization = organizations.find(o => o.id === project.resourceId);\n\t\t\t\tif (organization == null) continue;\n\n\t\t\t\tconst dashedPrefix = `${project.key}-`;\n\t\t\t\tconst underscoredPrefix = `${project.key}_`;\n\t\t\t\tautolinks.push({\n\t\t\t\t\tprefix: dashedPrefix,\n\t\t\t\t\turl: `${organization.url}/browse/${dashedPrefix}<num>`,\n\t\t\t\t\talphanumeric: false,\n\t\t\t\t\tignoreCase: false,\n\t\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\t\ttype: 'issue',\n\t\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\t\tdescriptor: { ...organization },\n\t\t\t\t});\n\t\t\t\tautolinks.push({\n\t\t\t\t\tprefix: underscoredPrefix,\n\t\t\t\t\turl: `${organization.url}/browse/${dashedPrefix}<num>`,\n\t\t\t\t\talphanumeric: false,\n\t\t\t\t\tignoreCase: false,\n\t\t\t\t\treferenceType: 'branch',\n\t\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\t\ttype: 'issue',\n\t\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\t\tdescriptor: { ...organization },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// A set missing a site's projects is served but not kept, so the next call tries that site again; one\n\t\t\t// built from projects read before the caches were dropped is not kept either.\n\t\t\tconst complete = metadata == null || metadata.completeness === 'complete';\n\t\t\tif (complete && this._projects.generation === generation) {\n\t\t\t\tthis._autolinks.set(accessToken, { autolinks: autolinks, builtAt: Date.now() });\n\t\t\t}\n\t\t\treturn autolinks;\n\t\t} catch (ex) {\n\t\t\t// Autolinks are decoration: keep serving the last set rather than failing the render that asked.\n\t\t\tLogger.error(ex, 'JiraIntegration.autolinks');\n\t\t\treturn this._autolinks.get(accessToken)?.autolinks ?? [];\n\t\t}\n\t}\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: JiraOrganizationDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getCurrentUserForResource(toTokenWithInfo(this.id, session), resource.id);\n\n\t\tif (user == null) return undefined;\n\t\treturn toAccount(user, this);\n\t}\n\n\t/** Atlassian's account id is the same on every site, so the first site answers for all of them. */\n\tprotected override async getProviderCurrentAccount(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<Account | undefined> {\n\t\tconst resource = (await this.getProviderResourcesForUser(session))?.[0];\n\t\treturn resource != null ? this.getProviderAccountForResource(session, resource) : undefined;\n\t}\n\n\t/** The site-list request the resource cache would otherwise answer; see `IntegrationBase.validateCredential`. */\n\tprotected override async validateCredential(session: ProviderAuthenticationSession): Promise<void> {\n\t\tconst api = await this.getProvidersApi();\n\t\tif ((await api.getJiraResourcesForCurrentUser(toTokenWithInfo(this.id, session))) == null) {\n\t\t\tthrow new Error('Jira did not confirm the credential');\n\t\t}\n\t}\n\n\t/**\n\t * Atlassian's `401` for a token missing a scope the request needs, which a reconnect fixes by consenting to it\n\t * again; see `IntegrationBase.isCredentialRefusal`. Atlassian gives the same answer to a malformed request URL,\n\t * so a request the SDK built wrong would read as the credential's too.\n\t */\n\tprotected override isCredentialRefusal(refusal: ProviderRefusal): boolean {\n\t\treturn refusal.status === 401 && /scope does not match/i.test(refusal.detail ?? '');\n\t}\n\n\t/**\n\t * The sites and the projects discovered under them, both of which expire and are dropped on a re-sync, so a site\n\t * or project created or deleted mid-session is picked up (#5907). Keyed by token, and projects by token and site.\n\t */\n\tprivate readonly _organizations = new DiscoveryCache<JiraOrganizationDescriptor[]>();\n\tprivate readonly _projects = new DiscoveryCache<JiraProjectDescriptor[]>();\n\t/** When the caches were last dropped; a persisted discovery read before then is as stale as they were. */\n\tprivate _discoveryInvalidatedAt = 0;\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<JiraOrganizationDescriptor[] | undefined> {\n\t\tconst { accessToken } = session;\n\n\t\tconst cachedResources = this._organizations.get(accessToken);\n\t\tif (cachedResources != null && !force) return cachedResources;\n\n\t\tconst generation = this._organizations.generation;\n\t\tconst api = await this.getProvidersApi();\n\t\tconst resources = await api.getJiraResourcesForCurrentUser(toTokenWithInfo(this.id, session));\n\t\tconst organizations = resources?.map(r => ({ ...r, key: r.id }));\n\t\tif (organizations == null) {\n\t\t\tthis._organizations.delete(accessToken);\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis._organizations.set(accessToken, organizations, { generation: generation });\n\t\treturn organizations;\n\t}\n\n\tprotected override async getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraOrganizationDescriptor[],\n\t\tforce: boolean = false,\n\t): Promise<JiraProjectDescriptor[] | undefined> {\n\t\treturn (await this.getProviderProjectsForResourcesWithMetadata(session, resources, force)).values;\n\t}\n\n\tprotected override async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraOrganizationDescriptor[],\n\t\tforce: boolean = false,\n\t): Promise<ProviderApiCollectionResult<JiraProjectDescriptor>> {\n\t\tconst { accessToken } = session;\n\t\tconst projectsCache = this._projects;\n\t\tconst generation = projectsCache.generation;\n\n\t\tlet resourcesWithoutProjects = [];\n\t\tif (force) {\n\t\t\tresourcesWithoutProjects = resources;\n\t\t} else {\n\t\t\tfor (const resource of resources) {\n\t\t\t\tconst resourceKey = `${accessToken}:${resource.id}`;\n\t\t\t\tconst cachedProjects = projectsCache.get(resourceKey);\n\t\t\t\tif (cachedProjects == null) {\n\t\t\t\t\tresourcesWithoutProjects.push(resource);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\tconst partialProjects: JiraProjectDescriptor[] = [];\n\t\tif (resourcesWithoutProjects.length > 0) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\t\tconst drains = await Promise.allSettled(\n\t\t\t\tresourcesWithoutProjects.map(async resource => ({\n\t\t\t\t\tresource: resource,\n\t\t\t\t\tresult: await collectProviderPagedResult(\n\t\t\t\t\t\tcursor => api.getJiraProjectsForResource(tokenWithInfo, resource.id, { cursor: cursor }),\n\t\t\t\t\t\tmaxPagesPerRequest,\n\t\t\t\t\t\t{ providerId: this.id, resourceId: resource.id },\n\t\t\t\t\t),\n\t\t\t\t})),\n\t\t\t);\n\n\t\t\tdrains.forEach((drain, index) => {\n\t\t\t\tif (drain.status === 'rejected') {\n\t\t\t\t\tconst resource = resourcesWithoutProjects[index];\n\t\t\t\t\tif (resource == null) return;\n\n\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\t\tfailures: [\n\t\t\t\t\t\t\ttoCollectionScopeFailure({ providerId: this.id, resourceId: resource.id }, drain.reason),\n\t\t\t\t\t\t],\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst { resource, result } = drain.value;\n\t\t\t\tmetadata = mergeCollectionMetadata(metadata, result.metadata);\n\t\t\t\tconst projects = result.values\n\t\t\t\t\t.filter(project => project.resourceId === resource.id)\n\t\t\t\t\t.map(project => ({ ...project }));\n\t\t\t\tconst incomplete =\n\t\t\t\t\tresult.truncated === true ||\n\t\t\t\t\t(result.metadata != null && result.metadata.completeness !== 'complete');\n\t\t\t\tif (incomplete) {\n\t\t\t\t\tpartialProjects.push(...projects);\n\t\t\t\t\tif (\n\t\t\t\t\t\tresult.truncated === true &&\n\t\t\t\t\t\t(result.metadata == null || result.metadata.completeness === 'complete')\n\t\t\t\t\t) {\n\t\t\t\t\t\tmetadata = mergeCollectionMetadata(metadata, { completeness: 'partial' });\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Not stored when the cache was dropped while this read was in flight, but still returned below: it\n\t\t\t\t// is what this read found, only no longer trusted for the next one.\n\t\t\t\tif (!projectsCache.set(`${accessToken}:${resource.id}`, projects, { generation: generation })) {\n\t\t\t\t\tpartialProjects.push(...projects);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tthis._autolinks.delete(accessToken);\n\t\t\t});\n\t\t}\n\n\t\tconst values = resources.reduce<JiraProjectDescriptor[]>((projects, resource) => {\n\t\t\tconst resourceProjects = projectsCache.get(`${accessToken}:${resource.id}`);\n\t\t\tif (resourceProjects != null) {\n\t\t\t\tprojects.push(...resourceProjects);\n\t\t\t}\n\t\t\treturn projects;\n\t\t}, partialProjects);\n\n\t\tconst projectsByIdentity = new Map<string, JiraProjectDescriptor>();\n\t\tfor (const project of values) {\n\t\t\tconst identity = `${project.resourceId}:${project.id}`;\n\t\t\tif (!projectsByIdentity.has(identity)) {\n\t\t\t\tprojectsByIdentity.set(identity, project);\n\t\t\t}\n\t\t}\n\t\treturn { values: [...projectsByIdentity.values()], metadata: metadata };\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\tconst api = await this.getProvidersApi();\n\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\t// By key, not name, as the multi-project search: JQL's `project = \"<value>\"` also accepts a name or id, so a\n\t\t// display name equal to another project's key would address that project. The key is the only unambiguous one.\n\t\tconst fetchPage: JiraIssuePageFetcher = (scope, cursor) =>\n\t\t\tapi.getIssuesForProjectPaged(tokenWithInfo, project.key, project.resourceId, {\n\t\t\t\t...scope,\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t\tsort: options?.sort,\n\t\t\t});\n\t\tconst drainOptions: JiraDrainOptions = {\n\t\t\tmaxPages: maxPagesPerRequest,\n\t\t\tfailureScope: { providerId: this.id, resourceId: project.resourceId, projectId: project.key },\n\t\t\tproviderName: 'Jira',\n\t\t\t// A project this token can no longer see (deleted, or its browse permission removed) holds no issues\n\t\t\t// for it, and the site's project list that named it is stale: drop that list so the next read\n\t\t\t// re-reads it, instead of failing the whole provider on every read until a restart (#5907).\n\t\t\tisEmptyFailure: ex => {\n\t\t\t\tif (!isJiraMissingProjectError(ex)) return false;\n\n\t\t\t\tLogger.warn(`Jira project '${project.key}' no longer exists or is not visible`);\n\t\t\t\tthis._projects.delete(`${session.accessToken}:${project.resourceId}`);\n\t\t\t\tthis._autolinks.delete(session.accessToken);\n\t\t\t\treturn true;\n\t\t\t},\n\t\t};\n\n\t\tif (options?.user != null) {\n\t\t\tconst read = await readUserScopedJiraIssues(\n\t\t\t\tfetchPage,\n\t\t\t\toptions.filters,\n\t\t\t\ttoUserIdentities(options.user, options),\n\t\t\t\tdrainOptions,\n\t\t\t);\n\t\t\tconst values = toUniqueJiraIssueShapes(read.issues, this);\n\t\t\tconst drain = { values: values, metadata: read.metadata, scope: drainOptions.failureScope };\n\t\t\treturn read.truncated ? { ...drain, truncated: true, recovery: 'none' } : { ...drain, truncated: false };\n\t\t}\n\n\t\tconst unscoped = await drainJiraIssues(fetchPage, {}, drainOptions);\n\t\tconst values = unscoped.issues\n\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'project' }))\n\t\t\t.filter((result): result is IssueShape => result !== undefined);\n\t\tconst drain = { values: values, metadata: unscoped.metadata, scope: drainOptions.failureScope };\n\t\treturn unscoped.status !== 'complete'\n\t\t\t? { ...drain, truncated: true, recovery: unscoped.status === 'backstop' ? 'narrow-scope' : 'none' }\n\t\t\t: { ...drain, truncated: false };\n\t}\n\n\t/**\n\t * Searches a site's projects together for a user-scoped read: one JQL names up to\n\t * {@link JIRA_MAX_PROJECT_KEYS_PER_REQUEST} projects of one site and one user scope, past which the SDK refuses\n\t * the call rather than risk a query string Jira rejects.\n\t *\n\t * An unscoped read is never searched across projects: it drains every issue in the project, and its backstop is\n\t * reported as `narrow-scope` per project, which one search across projects could not attribute.\n\t */\n\tprotected override getProjectIssuesSearches(\n\t\trequests: readonly ProjectIssuesRequest<JiraProjectDescriptor>[],\n\t): number[][] | undefined {\n\t\treturn groupProjectIssuesSearches(\n\t\t\trequests,\n\t\t\t({ project, options }) =>\n\t\t\t\toptions.user != null\n\t\t\t\t\t? [\n\t\t\t\t\t\t\tproject.resourceId,\n\t\t\t\t\t\t\toptions.user,\n\t\t\t\t\t\t\toptions.userId ?? '',\n\t\t\t\t\t\t\toptions.sort ?? '',\n\t\t\t\t\t\t\toptions.state ?? '',\n\t\t\t\t\t\t\toptions.filters?.join(',') ?? '',\n\t\t\t\t\t\t].join('\\0')\n\t\t\t\t\t: undefined,\n\t\t\tJIRA_MAX_PROJECT_KEYS_PER_REQUEST,\n\t\t);\n\t}\n\n\tprotected override async searchProviderProjectIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\trequests: readonly ProjectIssuesRequest<JiraProjectDescriptor>[],\n\t): Promise<IssueShape[][] | undefined> {\n\t\t// Every request of a search shares its site and user scope (see `getProjectIssuesSearches`).\n\t\tconst { project, options } = requests[0];\n\t\tif (options.user == null) return undefined;\n\t\t// One project's search is its own read, by the same key and user clauses, so it costs no more to read it as one;\n\t\t// and that read attributes a failure to the project and drops a project list that no longer has it.\n\t\tif (requests.length === 1) return undefined;\n\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst api = await this.getProvidersApi();\n\t\t// By key, not name: JQL resolves `project = \"<value>\"` against key first, so a name could read another\n\t\t// project's issues.\n\t\tconst projectKeys = requests.map(request => request.project.key);\n\t\tconst states = toProviderIssueStates(options.state);\n\n\t\treturn searchJiraProjectIssues(\n\t\t\t(userScope, cursor) =>\n\t\t\t\tapi.getIssuesForProjectsPaged(tokenWithInfo, projectKeys, project.resourceId, {\n\t\t\t\t\t...userScope,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tstates: states,\n\t\t\t\t\tsort: options.sort,\n\t\t\t\t}),\n\t\t\trequests.map(request => request.project.id),\n\t\t\toptions.filters,\n\t\t\ttoUserIdentities(options.user, options),\n\t\t\t{\n\t\t\t\tmaxPages: maxPagesPerRequest,\n\t\t\t\tfailureScope: { providerId: this.id, resourceId: project.resourceId },\n\t\t\t\tproviderName: 'Jira',\n\t\t\t},\n\t\t\tthis,\n\t\t);\n\t}\n\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: JiraOrganizationDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\tconst result = await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation);\n\t\t// This read can only return values, so what the account-wide read reports as missing is at least logged.\n\t\tfor (const failure of result?.metadata?.failures ?? []) {\n\t\t\tLogger.warn(\n\t\t\t\t`searchProviderMyIssues: Jira site '${failure.scope?.resourceId ?? '?'}' was not fully read: ${failure.message ?? failure.kind}`,\n\t\t\t);\n\t\t}\n\t\treturn result?.values;\n\t}\n\n\t/**\n\t * Overridden so `state` reaches the per-site read, which the default drops. Each site is drained on its own, so a\n\t * site that fails keeps what it read and is recorded as that site's failure instead of discarding the sites that\n\t * answered; so is an issue a page dropped because it could not be read. A page backstop or a stalled continuation\n\t * makes the read `truncated`. A refused site therefore goes through `confirmScopedAuthFailures`, which publishes it\n\t * as that site's auth warning or, if the credential itself is refused, as the connection's.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: JiraOrganizationDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tconst myResources = resources ?? (await this.getProviderResourcesForUser(session));\n\t\tif (!myResources) return undefined;\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\tconst results: IssueShape[] = [];\n\t\tlet truncated = false;\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\t// What each page reported failing, summarized at the end so a site whose many pages dropped issues reports\n\t\t// it once (see `summarizePageMetadata`); the pages keep the rest of their metadata, so `truncated` holds.\n\t\tconst pages: CollectionMetadata[] = [];\n\t\tconst readPage = async (resourceId: string, cursor: string | undefined) => {\n\t\t\tconst page = await api.getIssuesForResourceForCurrentUser(tokenWithInfo, resourceId, {\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t});\n\t\t\tif (!page.metadata?.failures?.length) return page;\n\n\t\t\tconst { failures: _failures, ...rest } = page.metadata;\n\t\t\tpages.push(page.metadata);\n\t\t\treturn { ...page, metadata: rest };\n\t\t};\n\t\tfor (const resource of myResources) {\n\t\t\tif (cancellation?.aborted) break;\n\n\t\t\tconst site = await collectProviderPagedResult(\n\t\t\t\tcursor =>\n\t\t\t\t\t// Cancellation is not truncation: the caller asked for the stop, so the site ends where it is.\n\t\t\t\t\tcancellation?.aborted\n\t\t\t\t\t\t? Promise.resolve({ values: [], paging: { cursor: '{}', more: false } })\n\t\t\t\t\t\t: readPage(resource.id, cursor),\n\t\t\t\tmaxPagesPerRequest,\n\t\t\t\t{ providerId: this.id, resourceId: resource.id },\n\t\t\t);\n\t\t\tresults.push(\n\t\t\t\t...site.values\n\t\t\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'account' }))\n\t\t\t\t\t.filter((result): result is IssueShape => result != null),\n\t\t\t);\n\t\t\ttruncated ||= site.truncated === true;\n\t\t\tmetadata = mergeCollectionMetadata(metadata, site.metadata);\n\t\t}\n\n\t\treturn {\n\t\t\tvalues: results,\n\t\t\ttruncated: truncated,\n\t\t\tmetadata: mergeCollectionMetadata(summarizePageMetadata(pages), metadata),\n\t\t};\n\t}\n\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: JiraOrganizationDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst issue = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: key,\n\t\t});\n\t\treturn issue != null ? toIssueShape(issue, this, { projection: 'point' }) : undefined;\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: JiraOrganizationDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst apiResult = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: id,\n\t\t});\n\t\tconst issue = apiResult != null ? toIssueShape(apiResult, this, { projection: 'point' }) : undefined;\n\t\treturn issue != null ? { ...issue, type: 'issue' } : undefined;\n\t}\n\n\tprotected override async getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresourceId: string,\n\t\tid: string,\n\t\tresourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\tif (resourceUrl == null) {\n\t\t\tthrow new Error('Jira direct issue reads require a resource URL');\n\t\t}\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst apiResult = await api.getJiraIssueByKey(toTokenWithInfo(this.id, session), resourceId, resourceUrl, id);\n\t\tconst issue = apiResult != null ? toIssueShape(apiResult, this, { projection: 'batch' }) : undefined;\n\t\treturn issue != null ? { ...issue, type: 'issue' } : undefined;\n\t}\n\n\t/**\n\t * The cheap check behind the batch issue read's etags: one bulk fetch per site and {@link jiraBulkFetchMaxKeys}\n\t * distinct keys, where {@link getProviderIssueByResourceId} sends one request per issue. A request that throws\n\t * rejects only its own targets' slots, with its error classified as the full read's would be.\n\t *\n\t * A target identified by a numeric issue id, which both reads accept as well as a key, is matched by the returned\n\t * issue's `id`; every other target by its key.\n\t *\n\t * Never proves an absence. Jira silently omits a key it can't find or show (listing it in no `issueErrors` either),\n\t * and answers a moved issue under its new key, which no longer matches the key asked for. Either way the target's\n\t * slot is rejected, so the full read, whose 404 does prove an absence, decides. When no target was answered and\n\t * nothing failed, the check declines instead, so a batch of only such keys costs a full read, not a failure.\n\t */\n\tprotected override async getProviderIssuesEtagFieldsByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\ttargets: readonly { resourceId: string; identifier: string; resourceUrl?: string }[],\n\t): Promise<PromiseSettledResult<IssueEtagFields | undefined>[] | undefined> {\n\t\tconst slots = new Array<PromiseSettledResult<IssueEtagFields | undefined>>(targets.length);\n\t\tlet answered = false;\n\t\tlet failed = false;\n\n\t\t// Each site's distinct keys, matched case-insensitively as Jira matches them, with the targets asking for each.\n\t\tconst bySite = new Map<string, Map<string, number[]>>();\n\t\tfor (const [index, target] of targets.entries()) {\n\t\t\tif (target.resourceUrl == null) {\n\t\t\t\t// Required as the full read requires it, so a check never answers for a target the full read refuses.\n\t\t\t\tslots[index] = {\n\t\t\t\t\tstatus: 'rejected',\n\t\t\t\t\treason: new Error('Jira direct issue reads require a resource URL'),\n\t\t\t\t};\n\t\t\t\tfailed = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet keys = bySite.get(target.resourceId);\n\t\t\tif (keys == null) {\n\t\t\t\tkeys = new Map();\n\t\t\t\tbySite.set(target.resourceId, keys);\n\t\t\t}\n\n\t\t\tconst key = target.identifier.toUpperCase();\n\t\t\tlet indices = keys.get(key);\n\t\t\tif (indices == null) {\n\t\t\t\tindices = [];\n\t\t\t\tkeys.set(key, indices);\n\t\t\t}\n\t\t\tindices.push(index);\n\t\t}\n\n\t\tconst requests = [...bySite].flatMap(([resourceId, keys]) =>\n\t\t\tchunk([...keys], jiraBulkFetchMaxKeys).map(entries => ({ resourceId: resourceId, entries: entries })),\n\t\t);\n\t\tif (!requests.length) return slots;\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst answers = await mapSettledBounded(requests, providerFanOutConcurrency, r =>\n\t\t\tapi.getJiraIssuesEtagFields(\n\t\t\t\ttokenWithInfo,\n\t\t\t\tr.resourceId,\n\t\t\t\tr.entries.map(([key]) => key),\n\t\t\t),\n\t\t);\n\n\t\tfor (const [i, answer] of answers.entries()) {\n\t\t\tconst { entries } = requests[i];\n\t\t\tif (answer.status === 'rejected') {\n\t\t\t\tfailed = true;\n\t\t\t\tfor (const [, indices] of entries) {\n\t\t\t\t\tfor (const index of indices) {\n\t\t\t\t\t\tslots[index] = answer;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (answer.value.errorCount > 0) {\n\t\t\t\tfailed = true;\n\t\t\t}\n\n\t\t\t// By key or id, never by position: Jira returns the issues in its own order, and only the ones it found.\n\t\t\tconst byKey = new Map<string, JiraIssueEtagResponse>();\n\t\t\tconst byId = new Map<string, JiraIssueEtagResponse>();\n\t\t\tfor (const issue of answer.value.issues) {\n\t\t\t\tbyKey.set(issue.key.toUpperCase(), issue);\n\t\t\t\tbyId.set(issue.id, issue);\n\t\t\t}\n\n\t\t\tfor (const [key, indices] of entries) {\n\t\t\t\t// A key target never matches by id, so an issue returned under a different key (moved) is rejected.\n\t\t\t\tconst issue = numericIssueId.test(key) ? byId.get(key) : byKey.get(key);\n\t\t\t\tlet slot: PromiseSettledResult<IssueEtagFields | undefined>;\n\t\t\t\tif (issue == null) {\n\t\t\t\t\tslot = { status: 'rejected', reason: new Error(`Jira bulk fetch did not return ${key}`) };\n\t\t\t\t} else {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tslot = { status: 'fulfilled', value: toJiraIssueEtagFields(issue) };\n\t\t\t\t\t\tanswered = true;\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\tslot = { status: 'rejected', reason: ex };\n\t\t\t\t\t\tfailed = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor (const index of indices) {\n\t\t\t\t\tslots[index] = slot;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn answered || failed ? slots : undefined;\n\t}\n\n\tprotected override async providerOnConnect(): Promise<void> {\n\t\tthis._autolinks.clear();\n\t\tif (this._session == null) return;\n\n\t\tconst session = this._session;\n\t\tconst { accessToken } = session;\n\t\tconst organizationsGeneration = this._organizations.generation;\n\t\tconst projectsGeneration = this._projects.generation;\n\n\t\t// Persisted discovery seeds the caches only while it would still be fresh in them: read within the TTL, and\n\t\t// after the caches were last dropped. Otherwise a re-sync, which runs this again, would put back the very\n\t\t// project list it just dropped, and a restart would trust one read any time before (#5907).\n\t\tconst isFresh = (timestamp: unknown): timestamp is number =>\n\t\t\ttypeof timestamp === 'number' &&\n\t\t\ttimestamp > this._discoveryInvalidatedAt &&\n\t\t\tDate.now() - timestamp < discoveryCacheTtl;\n\n\t\tconst storedOrganizations = this.ctx.storage.get(`jira:${accessToken}:organizations`);\n\t\t// Projects persisted without their sites belong to a session whose sites were cleared below; ignore them.\n\t\tconst storedProjects =\n\t\t\tstoredOrganizations != null ? this.ctx.storage.get(`jira:${accessToken}:projects`) : undefined;\n\n\t\tlet organizations: JiraOrganizationDescriptor[] | undefined;\n\t\tif (storedOrganizations?.data != null && isFresh(storedOrganizations.timestamp)) {\n\t\t\tconst seeded = storedOrganizations.data.map((o: JiraOrganizationDescriptor) => ({ ...o }));\n\t\t\torganizations = seeded;\n\t\t\tthis._organizations.set(accessToken, seeded, {\n\t\t\t\tgeneration: organizationsGeneration,\n\t\t\t\tstoredAt: storedOrganizations.timestamp,\n\t\t\t});\n\t\t} else {\n\t\t\t// Stamped with when the read started, so the TTL counts from what it actually saw.\n\t\t\tconst readAt = Date.now();\n\t\t\torganizations = await this.getProviderResourcesForUser(session, true);\n\t\t\tif (storedOrganizations == null) {\n\t\t\t\t// Clear all other stored organizations and projects when our session changes\n\t\t\t\tawait this.ctx.storage.deleteWithPrefix('jira');\n\t\t\t}\n\t\t\t// Not persisted when the caches were dropped while it was in flight: it is then a read from before the\n\t\t\t// refresh, and the next start, which knows nothing of that refresh, would take it for fresh.\n\t\t\tif (this._organizations.generation === organizationsGeneration) {\n\t\t\t\tawait this.ctx.storage.store(`jira:${accessToken}:organizations`, {\n\t\t\t\t\tv: 1,\n\t\t\t\t\ttimestamp: readAt,\n\t\t\t\t\tdata: organizations,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tif (storedProjects?.data != null && isFresh(storedProjects.timestamp)) {\n\t\t\tconst projectsByResource = new Map<string, JiraProjectDescriptor[]>();\n\t\t\tfor (const project of storedProjects.data as JiraProjectDescriptor[]) {\n\t\t\t\tconst projects = projectsByResource.get(project.resourceId);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tprojectsByResource.set(project.resourceId, [{ ...project }]);\n\t\t\t\t} else if (!projects.some(p => p.id === project.id)) {\n\t\t\t\t\tprojects.push({ ...project });\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const [resourceId, projects] of projectsByResource) {\n\t\t\t\tthis._projects.set(`${accessToken}:${resourceId}`, projects, {\n\t\t\t\t\tgeneration: projectsGeneration,\n\t\t\t\t\tstoredAt: storedProjects.timestamp,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (organizations?.length) {\n\t\t\tconst readAt = Date.now();\n\t\t\tconst projects = await this.getProviderProjectsForResources(session, organizations, true);\n\t\t\t// See the organizations above.\n\t\t\tif (this._projects.generation === projectsGeneration) {\n\t\t\t\tawait this.ctx.storage.store(`jira:${accessToken}:projects`, {\n\t\t\t\t\tv: 1,\n\t\t\t\t\ttimestamp: readAt,\n\t\t\t\t\tdata: projects,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._discoveryInvalidatedAt = Date.now();\n\t\tthis._organizations.clear();\n\t\tthis._projects.clear();\n\t\tthis._autolinks.clear();\n\t}\n\n\tprotected override providerOnDisconnect(): void {\n\t\tthis._organizations.clear();\n\t\tthis._projects.clear();\n\t\tthis._autolinks.clear();\n\t}\n}\n"],"mappings":"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"}
@@ -677,8 +677,7 @@ export interface ProviderMetadata {
677
677
  */
678
678
  supportedAccountWideIssueSorts?: IssueSorting[];
679
679
  /**
680
- * Whether an issue tracker's reads (the project-scoped read, which is `listIssueTrackerIssuesPage`, and its
681
- * account-wide read) can be asked for a `state` other than open, and honor it server-side.
680
+ * Whether issue list reads (including Azure project/repository and account-wide reads) can be asked for a `state` other than open, and honor it server-side.
682
681
  *
683
682
  * A tracker read is open-only unless asked otherwise, so absent does not mean the read returns every state: it
684
683
  * means only `'open'` is expressible, and `'closed'`/`'all'` are refused rather than served as the open list.
@@ -399,6 +399,7 @@ const providersMetadata = {
399
399
  scopes: ["Project (Read)", "Repository (Write)"]
400
400
  },
401
401
  ["azureDevOps"]: {
402
+ supportsIssueStates: true,
402
403
  domain: "dev.azure.com",
403
404
  id: "azureDevOps",
404
405
  name: "Azure DevOps",
@@ -433,6 +434,7 @@ const providersMetadata = {
433
434
  ]
434
435
  },
435
436
  ["azure-devops-server"]: {
437
+ supportsIssueStates: true,
436
438
  domain: "",
437
439
  id: "azure-devops-server",
438
440
  name: "Azure DevOps Server",
@@ -584,9 +586,14 @@ function toIssueProviderState(state) {
584
586
  category: state.category
585
587
  };
586
588
  }
589
+ function isProviderIssueClosed(issue, providerId) {
590
+ const category = issue.state?.category;
591
+ if ((providerId === "azureDevOps" || providerId === "azure-devops-server") && category != null) return isTerminalIssueStateCategory(category);
592
+ return issue.closedDate != null || isTerminalIssueStateCategory(category);
593
+ }
587
594
  function toIssueShape(issue, provider, options) {
588
595
  if (issue.updatedDate == null || issue.url == null) return void 0;
589
- const closed = issue.closedDate != null || isTerminalIssueStateCategory(issue.state?.category);
596
+ const closed = isProviderIssueClosed(issue, provider.id);
590
597
  return {
591
598
  type: "issue",
592
599
  provider,
@@ -956,7 +963,7 @@ function authoredByCurrentAccount(author, provider, currentAccount) {
956
963
  }
957
964
  function fromProviderIssue(issue, integration, options) {
958
965
  const identifier = toIssueIdentifier(issue.number);
959
- const closed = issue.closedDate != null || isTerminalIssueStateCategory(issue.state?.category);
966
+ const closed = isProviderIssueClosed(issue, integration.id);
960
967
  return new Issue(integration, identifier, issue.graphQLId ?? issue.id, issue.title, issue.url ?? "", issue.createdDate, issue.updatedDate ?? issue.closedDate ?? issue.createdDate, closed, closed ? "closed" : "opened", fromProviderAccount(issue.author), issue.assignees?.map(fromProviderAccount) ?? void 0, issue.repository != null ? {
961
968
  owner: issue.repository.owner.login ?? "",
962
969
  repo: issue.repository.name,