@gitkraken/core-gitlens 0.23.0 → 0.25.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 (152) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +23 -15
  3. package/dist/git/errors.d.ts +6 -5
  4. package/dist/git/errors.js +7 -6
  5. package/dist/git/errors.js.map +1 -1
  6. package/dist/git/features.d.ts +1 -1
  7. package/dist/git/features.js +1 -1
  8. package/dist/git/features.js.map +1 -1
  9. package/dist/git/models/remoteProvider.d.ts +4 -1
  10. package/dist/git/models/remoteProvider.js.map +1 -1
  11. package/dist/git/providers/provider.d.ts +4 -2
  12. package/dist/git/utils/branch.utils.d.ts +4 -1
  13. package/dist/git/utils/branch.utils.js +5 -2
  14. package/dist/git/utils/branch.utils.js.map +1 -1
  15. package/dist/git/utils/mergeConflicts.utils.js +2 -2
  16. package/dist/git/utils/mergeConflicts.utils.js.map +1 -1
  17. package/dist/git-cli/cliGitProvider.d.ts +14 -3
  18. package/dist/git-cli/cliGitProvider.js +71 -34
  19. package/dist/git-cli/cliGitProvider.js.map +1 -1
  20. package/dist/git-cli/exec/git.d.ts +7 -1
  21. package/dist/git-cli/exec/git.js +19 -4
  22. package/dist/git-cli/exec/git.js.map +1 -1
  23. package/dist/git-cli/providers/branchRemote.js +2 -5
  24. package/dist/git-cli/providers/branchRemote.js.map +1 -1
  25. package/dist/git-cli/providers/branches.js +4 -4
  26. package/dist/git-cli/providers/branches.js.map +1 -1
  27. package/dist/git-cli/providers/config.js +6 -3
  28. package/dist/git-cli/providers/config.js.map +1 -1
  29. package/dist/git-cli/providers/pausedOperations.js +2 -2
  30. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  31. package/dist/git-cli/providers/staging.js +1 -1
  32. package/dist/ipc/discovery.js +1 -1
  33. package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
  34. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  35. package/dist/plus/ai/errors.d.ts +4 -1
  36. package/dist/plus/ai/errors.js +4 -0
  37. package/dist/plus/ai/errors.js.map +1 -1
  38. package/dist/plus/ai/providers/mistralProvider.js +1 -1
  39. package/dist/plus/ai/providers/mistralProvider.js.map +1 -1
  40. package/dist/plus/ai/providers/openAICompatibleProviderBase.js +2 -1
  41. package/dist/plus/ai/providers/openAICompatibleProviderBase.js.map +1 -1
  42. package/dist/plus/git-github/api/github.js +12 -13
  43. package/dist/plus/git-github/api/github.js.map +1 -1
  44. package/dist/plus/git-github/api/token.d.ts +2 -0
  45. package/dist/plus/integrations/authentication/cloudIntegrationService.d.ts +1 -1
  46. package/dist/plus/integrations/authentication/cloudIntegrationService.js +6 -1
  47. package/dist/plus/integrations/authentication/cloudIntegrationService.js.map +1 -1
  48. package/dist/plus/integrations/authentication/configuredIntegrationService.js +2 -2
  49. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +18 -5
  50. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
  51. package/dist/plus/integrations/authentication/integrationAuthenticationService.d.ts +3 -1
  52. package/dist/plus/integrations/authentication/integrationAuthenticationService.js +4 -0
  53. package/dist/plus/integrations/authentication/integrationAuthenticationService.js.map +1 -1
  54. package/dist/plus/integrations/authentication/models.d.ts +2 -0
  55. package/dist/plus/integrations/authentication/models.js +2 -1
  56. package/dist/plus/integrations/authentication/models.js.map +1 -1
  57. package/dist/plus/integrations/authentication/observedAuthenticationProvider.js +1 -1
  58. package/dist/plus/integrations/authentication/rejectedTokenTracker.d.ts +5 -4
  59. package/dist/plus/integrations/authentication/rejectedTokenTracker.js +5 -4
  60. package/dist/plus/integrations/authentication/rejectedTokenTracker.js.map +1 -1
  61. package/dist/plus/integrations/index.d.ts +2 -2
  62. package/dist/plus/integrations/index.js +1 -1
  63. package/dist/plus/integrations/index.js.map +1 -1
  64. package/dist/plus/integrations/integrationService.d.ts +6 -0
  65. package/dist/plus/integrations/integrationService.js +3 -3
  66. package/dist/plus/integrations/integrationService.js.map +1 -1
  67. package/dist/plus/integrations/lite.js +1 -1
  68. package/dist/plus/integrations/manager.d.ts +51 -3
  69. package/dist/plus/integrations/models/gitHostIntegration.d.ts +14 -0
  70. package/dist/plus/integrations/models/gitHostIntegration.js +50 -41
  71. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  72. package/dist/plus/integrations/models/integration.d.ts +27 -5
  73. package/dist/plus/integrations/models/integration.js +57 -15
  74. package/dist/plus/integrations/models/integration.js.map +1 -1
  75. package/dist/plus/integrations/models/issueReads.d.ts +6 -0
  76. package/dist/plus/integrations/models/issuesIntegration.js +11 -11
  77. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  78. package/dist/plus/integrations/providers/azure/azure.js +2 -2
  79. package/dist/plus/integrations/providers/azureDevOps.js +1 -1
  80. package/dist/plus/integrations/providers/azureDevOpsServer.js +1 -1
  81. package/dist/plus/integrations/providers/bitbucket/bitbucket.js +1 -1
  82. package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -1
  83. package/dist/plus/integrations/providers/bitbucket-server.js +1 -1
  84. package/dist/plus/integrations/providers/bitbucket.js +1 -1
  85. package/dist/plus/integrations/providers/github.js +1 -1
  86. package/dist/plus/integrations/providers/gitlab/gitlab.js +2 -2
  87. package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -1
  88. package/dist/plus/integrations/providers/gitlab.js +1 -1
  89. package/dist/plus/integrations/providers/jira-server.js +2 -2
  90. package/dist/plus/integrations/providers/jira.js +1 -1
  91. package/dist/plus/integrations/providers/linear.d.ts +4 -4
  92. package/dist/plus/integrations/providers/linear.js +26 -9
  93. package/dist/plus/integrations/providers/linear.js.map +1 -1
  94. package/dist/plus/integrations/providers/models.js +2 -1
  95. package/dist/plus/integrations/providers/models.js.map +1 -1
  96. package/dist/plus/integrations/providers/providerErrors.js +9 -0
  97. package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
  98. package/dist/plus/integrations/providers/providersApi.d.ts +2 -0
  99. package/dist/plus/integrations/providers/providersApi.js +1 -1
  100. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  101. package/dist/plus/integrations/providers/trello.js +1 -1
  102. package/dist/plus/integrations/providers/utils.js +1 -1
  103. package/dist/plus/integrations/reads/counts.js +1 -1
  104. package/dist/plus/integrations/reads/cursors.js +1 -1
  105. package/dist/plus/integrations/reads/drains.d.ts +5 -4
  106. package/dist/plus/integrations/reads/drains.js +28 -8
  107. package/dist/plus/integrations/reads/drains.js.map +1 -1
  108. package/dist/plus/integrations/reads/hierarchy.js +1 -1
  109. package/dist/plus/integrations/reads/issueBatch.d.ts +1 -1
  110. package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
  111. package/dist/plus/integrations/reads/issueTracker.d.ts +4 -1
  112. package/dist/plus/integrations/reads/issueTracker.js +1 -1
  113. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  114. package/dist/plus/integrations/reads/issues.d.ts +2 -0
  115. package/dist/plus/integrations/reads/issues.js +5 -3
  116. package/dist/plus/integrations/reads/issues.js.map +1 -1
  117. package/dist/plus/integrations/reads/paging.d.ts +5 -6
  118. package/dist/plus/integrations/reads/paging.js +5 -6
  119. package/dist/plus/integrations/reads/paging.js.map +1 -1
  120. package/dist/plus/integrations/reads/pullRequests.d.ts +4 -0
  121. package/dist/plus/integrations/reads/pullRequests.js +4 -2
  122. package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
  123. package/dist/plus/integrations/reads/resolveRepository.js +1 -1
  124. package/dist/plus/integrations/reads/sweeps.js +42 -10
  125. package/dist/plus/integrations/reads/sweeps.js.map +1 -1
  126. package/dist/plus/integrations/results.d.ts +23 -3
  127. package/dist/plus/integrations/results.js.map +1 -1
  128. package/dist/plus/notifications/api.d.ts +20 -0
  129. package/dist/plus/notifications/api.js +89 -0
  130. package/dist/plus/notifications/api.js.map +1 -0
  131. package/dist/plus/notifications/context.d.ts +28 -0
  132. package/dist/plus/notifications/context.js +0 -0
  133. package/dist/plus/notifications/errors.d.ts +13 -0
  134. package/dist/plus/notifications/errors.js +22 -0
  135. package/dist/plus/notifications/errors.js.map +1 -0
  136. package/dist/plus/notifications/models.d.ts +94 -0
  137. package/dist/plus/notifications/models.js +0 -0
  138. package/dist/plus/notifications/notificationsService.d.ts +115 -0
  139. package/dist/plus/notifications/notificationsService.js +381 -0
  140. package/dist/plus/notifications/notificationsService.js.map +1 -0
  141. package/dist/plus/notifications/parse.d.ts +12 -0
  142. package/dist/plus/notifications/parse.js +152 -0
  143. package/dist/plus/notifications/parse.js.map +1 -0
  144. package/dist/plus/notifications/presentation.d.ts +28 -0
  145. package/dist/plus/notifications/presentation.js +55 -0
  146. package/dist/plus/notifications/presentation.js.map +1 -0
  147. package/dist/utils/env/node/exec.d.ts +10 -1
  148. package/dist/utils/env/node/exec.js +41 -4
  149. package/dist/utils/env/node/exec.js.map +1 -1
  150. package/docs/integrations.md +25 -3
  151. package/docs/notifications.md +239 -0
  152. package/package.json +22 -2
@@ -13,6 +13,8 @@ export interface GitHubTokenInfo {
13
13
  readonly type: string | undefined;
14
14
  readonly scopes?: readonly string[];
15
15
  readonly expiresAt?: Date;
16
+ /** The token's connection, which a failed request's strike counts against (the primary when absent). */
17
+ readonly connectionId?: string;
16
18
  }
17
19
  //#endregion
18
20
  //# sourceMappingURL=token.d.ts.map
@@ -15,7 +15,7 @@ export declare class CloudIntegrationService {
15
15
  * failure, which they must catch and treat as retryable instead of disconnecting (#5569).
16
16
  */
17
17
  getConnectionSession(id: IntegrationIds, refreshToken?: string, connectionId?: string): Promise<CloudIntegrationAuthenticationSession | undefined>;
18
- disconnect(id: IntegrationIds): Promise<boolean>;
18
+ disconnect(id: IntegrationIds, connectionId?: string): Promise<boolean>;
19
19
  /**
20
20
  * Disconnects a single connection by its token id (multi-account). The backend promotes a secondary
21
21
  * to primary when the removed connection was the primary, so the provider stays connected if others
@@ -1,5 +1,6 @@
1
1
  import { getScopedLogger } from "../../../utils/logger.scoped.js";
2
2
  import { RequestRateLimitError } from "../../../git/errors.js";
3
+ import { isSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
3
4
  import { toCloudIntegrationType } from "./models.js";
4
5
  //#region ../plus/integrations/src/authentication/cloudIntegrationService.ts
5
6
  /**
@@ -134,7 +135,11 @@ var CloudIntegrationService = class {
134
135
  const data = (await tokenRsp.json())?.data;
135
136
  return data != null ? toSession(data) : void 0;
136
137
  }
137
- async disconnect(id) {
138
+ async disconnect(id, connectionId) {
139
+ if (isSelfManagedHostIntegrationId(id)) {
140
+ if (!connectionId?.trim()) return false;
141
+ return this.disconnectConnection(id, connectionId);
142
+ }
138
143
  const scope = getScopedLogger();
139
144
  const cloudIntegrationType = toCloudIntegrationType[id];
140
145
  if (cloudIntegrationType == null) {
@@ -1 +1 @@
1
- {"version":3,"file":"cloudIntegrationService.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/cloudIntegrationService.ts"],"sourcesContent":["import { RequestRateLimitError } from '@gitlens/git/errors.js';\nimport { getScopedLogger } from '@gitlens/utils/logger.scoped.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport type {\n\tCloudIntegrationAuthenticationSession,\n\tCloudIntegrationConnection,\n\tCloudIntegrationType,\n} from './models.js';\nimport { toCloudIntegrationType } from './models.js';\n\n/**\n * Wire shape of a `v1/provider-tokens` connection descriptor. The list endpoint returns one such\n * entry per provider as the primary, with additional accounts nested under `secondaries`. Field casing\n * is the backend's (camelCase, `tokenId`).\n */\ninterface GKProviderTokenData {\n\ttokenId: string;\n\tprovider: CloudIntegrationType;\n\ttype: CloudIntegrationAuthenticationSession['type'];\n\tdomain: string;\n\t/** Human-readable account handle; the backend includes it on the connection when available. */\n\taccountName?: string;\n}\n\n/** Wire shape of a single `v1/provider-tokens/...` token response (`data`). */\ninterface GKProviderToken {\n\ttokenId?: string;\n\tisPrimary?: boolean;\n\taccessToken: string;\n\tappKey?: string;\n\texpiresIn: number;\n\tscopes: string;\n\ttype: CloudIntegrationAuthenticationSession['type'];\n\tdomain?: string;\n}\n\n/**\n * Whether the backend definitively answered that this connection has no fetchable token, so it can be\n * cleanly disconnected (#5497). Everything else is retryable — notably 401/403, which reject our GK\n * *account* token rather than the connection. The asymmetry is deliberate: a wrongly-kept descriptor\n * self-corrects on the next sync, a wrongly-dropped one needs a manual reconnect (#5569).\n */\nfunction isTerminalStatus(status: number): boolean {\n\tswitch (status) {\n\t\tcase 400: // e.g. a non-uuid token id (#5497)\n\t\tcase 404:\n\t\tcase 410:\n\t\tcase 422:\n\t\t\treturn true;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * The error for a token fetch the backend refused for a reason a retry can fix. A 429 is typed as a rate limit so\n * a read that needed the token reports a throttle rather than a broken connection (gitkraken/kepler#3546); every\n * other status stays a plain error.\n */\nfunction retryableTokenFetchError(status: number, message: string): Error {\n\tconst error = new Error(message);\n\treturn status === 429 ? new RequestRateLimitError(error, undefined, undefined) : error;\n}\n\nfunction toSession(data: GKProviderToken): CloudIntegrationAuthenticationSession {\n\t// Normalize the backend's `tokenId` onto our `id` so callers get a stable per-connection identity.\n\treturn {\n\t\ttype: data.type,\n\t\taccessToken: data.accessToken,\n\t\tdomain: data.domain ?? '',\n\t\texpiresIn: data.expiresIn,\n\t\tscopes: data.scopes,\n\t\tid: data.tokenId,\n\t\tappKey: data.appKey,\n\t};\n}\n\nasync function getResponseError(response: Response): Promise<unknown> {\n\ttry {\n\t\treturn ((await response.json()) as { error?: unknown })?.error;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport class CloudIntegrationService {\n\tconstructor(private readonly ctx: IntegrationServiceContext) {}\n\n\tasync getConnections(): Promise<CloudIntegrationConnection[] | undefined> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst providersRsp = await this.ctx.account.fetchGkApi('v1/provider-tokens', { method: 'GET' });\n\t\tif (!providersRsp.ok) {\n\t\t\tconst error = await getResponseError(providersRsp);\n\t\t\tconst errorMessage =\n\t\t\t\ttypeof error === 'string'\n\t\t\t\t\t? error\n\t\t\t\t\t: ((error as { message?: string })?.message ?? providersRsp.statusText);\n\t\t\tif (error != null) {\n\t\t\t\tscope?.error(undefined, `Failed to get connected providers from cloud: ${errorMessage}`);\n\t\t\t}\n\t\t\tthis.ctx.hooks?.connection?.onConnectionsFetchFailed?.({ code: providersRsp.status });\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst data = ((await providersRsp.json()) as { data?: unknown })?.data as\n\t\t\t| (GKProviderTokenData & { secondaries?: GKProviderTokenData[] })[]\n\t\t\t| undefined;\n\t\t// A non-array `data` (unexpected backend shape) would throw on iteration below and abort the\n\t\t// whole cloud-sync path, so bail gracefully instead.\n\t\tif (!Array.isArray(data)) return undefined;\n\n\t\t// Flatten the primary + `secondaries[]` grouping into one connection per account. Primacy is\n\t\t// positional on the wire (top-level entry = primary), so we set the `primary` flag here.\n\t\tconst connections: CloudIntegrationConnection[] = [];\n\t\tfor (const item of data) {\n\t\t\tconnections.push({\n\t\t\t\tid: item.tokenId,\n\t\t\t\ttype: item.type,\n\t\t\t\tprovider: item.provider,\n\t\t\t\tdomain: item.domain,\n\t\t\t\tprimary: true,\n\t\t\t\taccountName: item.accountName,\n\t\t\t});\n\t\t\t// `secondaries` comes from the same unknown payload as `data`; guard against a non-array shape so\n\t\t\t// an unexpected value doesn't throw here and abort the whole cloud-sync path.\n\t\t\tfor (const secondary of Array.isArray(item.secondaries) ? item.secondaries : []) {\n\t\t\t\tconnections.push({\n\t\t\t\t\tid: secondary.tokenId,\n\t\t\t\t\ttype: secondary.type,\n\t\t\t\t\tprovider: secondary.provider ?? item.provider,\n\t\t\t\t\tdomain: secondary.domain || item.domain,\n\t\t\t\t\tprimary: false,\n\t\t\t\t\taccountName: secondary.accountName,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn connections;\n\t}\n\n\t/**\n\t * Fetches a connection's token, refreshing it first when `refreshToken` is given; `connectionId` targets\n\t * a specific connection (multi-account) rather than the provider's primary.\n\t *\n\t * Resolves `undefined` ONLY on a terminal failure (see {@link isTerminalStatus}, or an ok response with\n\t * no `data`), which callers may treat as \"the connection is gone\" (#5497). THROWS on every other\n\t * failure, which they must catch and treat as retryable instead of disconnecting (#5569).\n\t */\n\tasync getConnectionSession(\n\t\tid: IntegrationIds,\n\t\trefreshToken?: string,\n\t\tconnectionId?: string,\n\t): Promise<CloudIntegrationAuthenticationSession | undefined> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst refresh = Boolean(refreshToken);\n\t\tconst cloudIntegrationType = toCloudIntegrationType[id];\n\t\tif (cloudIntegrationType == null) {\n\t\t\tscope?.error(undefined, `Unsupported cloud integration type: ${id}`);\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst reqInitOptions = refreshToken\n\t\t\t? {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\taccess_token: refreshToken,\n\t\t\t\t\t}),\n\t\t\t\t}\n\t\t\t: { method: 'GET' };\n\n\t\t// A specific connection (multi-account) is addressed by its token id under `/tokens/{tokenId}`,\n\t\t// including refresh (`/tokens/{tokenId}/refresh`). The provider-scoped endpoints only ever operate\n\t\t// on the provider's PRIMARY connection, so they must NOT be used to refresh a secondary.\n\t\tconst path = connectionId\n\t\t\t? `v1/provider-tokens/tokens/${encodeURIComponent(connectionId)}${refresh ? '/refresh' : ''}`\n\t\t\t: `v1/provider-tokens/${cloudIntegrationType}${refresh ? '/refresh' : ''}`;\n\t\tconst tokenRsp = await this.ctx.account.fetchGkApi(path, reqInitOptions);\n\t\tif (!tokenRsp.ok) {\n\t\t\tconst error = await getResponseError(tokenRsp);\n\t\t\tconst errorMessage =\n\t\t\t\ttypeof error === 'string' ? error : ((error as { message?: string })?.message ?? tokenRsp.statusText);\n\t\t\tif (error != null) {\n\t\t\t\tscope?.error(\n\t\t\t\t\tundefined,\n\t\t\t\t\t`Failed to ${refresh ? 'refresh' : 'get'} ${id} token from cloud: ${errorMessage}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthis.ctx.hooks?.connection?.onConnectionFetchFailed?.({\n\t\t\t\tid: id,\n\t\t\t\tcode: tokenRsp.status,\n\t\t\t\trefreshing: refresh,\n\t\t\t});\n\n\t\t\tif (refresh) {\n\t\t\t\t// try once to just get the latest token if the refresh fails, and give up if that fails too;\n\t\t\t\t// stay scoped to the same connection (by token id) so we don't fall back to the primary.\n\t\t\t\tconst latestPath = connectionId\n\t\t\t\t\t? `v1/provider-tokens/tokens/${encodeURIComponent(connectionId)}`\n\t\t\t\t\t: `v1/provider-tokens/${cloudIntegrationType}`;\n\t\t\t\tconst newTokenRsp = await this.ctx.account.fetchGkApi(latestPath, { method: 'GET' });\n\t\t\t\tif (newTokenRsp.ok) {\n\t\t\t\t\tconst data = ((await newTokenRsp.json()) as { data?: GKProviderToken })?.data;\n\t\t\t\t\treturn data != null ? toSession(data) : undefined;\n\t\t\t\t}\n\n\t\t\t\t// Report the fallback's own status (as a get, not a refresh) — it's what decides the outcome.\n\t\t\t\tthis.ctx.hooks?.connection?.onConnectionFetchFailed?.({\n\t\t\t\t\tid: id,\n\t\t\t\t\tcode: newTokenRsp.status,\n\t\t\t\t\trefreshing: false,\n\t\t\t\t});\n\n\t\t\t\tif (isTerminalStatus(newTokenRsp.status)) return undefined;\n\n\t\t\t\tthrow retryableTokenFetchError(\n\t\t\t\t\tnewTokenRsp.status,\n\t\t\t\t\t`Retryable failure (${newTokenRsp.status}) refreshing ${id} token from cloud`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (isTerminalStatus(tokenRsp.status)) return undefined;\n\n\t\t\tthrow retryableTokenFetchError(\n\t\t\t\ttokenRsp.status,\n\t\t\t\t`Retryable failure (${tokenRsp.status}) getting ${id} token from cloud`,\n\t\t\t);\n\t\t}\n\n\t\tconst data = ((await tokenRsp.json()) as { data?: GKProviderToken })?.data;\n\t\treturn data != null ? toSession(data) : undefined;\n\t}\n\n\tasync disconnect(id: IntegrationIds): Promise<boolean> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst cloudIntegrationType = toCloudIntegrationType[id];\n\t\tif (cloudIntegrationType == null) {\n\t\t\tscope?.error(undefined, `Unsupported cloud integration type: ${id}`);\n\t\t\treturn false;\n\t\t}\n\n\t\tconst tokenRsp = await this.ctx.account.fetchGkApi(`v1/provider-tokens/${cloudIntegrationType}`, {\n\t\t\tmethod: 'DELETE',\n\t\t});\n\t\tif (!tokenRsp.ok) {\n\t\t\tconst error = await getResponseError(tokenRsp);\n\t\t\tconst errorMessage =\n\t\t\t\ttypeof error === 'string' ? error : ((error as { message?: string })?.message ?? tokenRsp.statusText);\n\t\t\tif (error != null) {\n\t\t\t\tscope?.error(undefined, `Failed to disconnect ${id} token from cloud: ${errorMessage}`);\n\t\t\t}\n\t\t\tthis.ctx.hooks?.connection?.onDisconnectFailed?.({\n\t\t\t\tid: id,\n\t\t\t\tcode: tokenRsp.status,\n\t\t\t});\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Disconnects a single connection by its token id (multi-account). The backend promotes a secondary\n\t * to primary when the removed connection was the primary, so the provider stays connected if others\n\t * remain. Returns whether the delete succeeded.\n\t */\n\tasync disconnectConnection(id: IntegrationIds, connectionId: string): Promise<boolean> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst tokenRsp = await this.ctx.account.fetchGkApi(\n\t\t\t`v1/provider-tokens/tokens/${encodeURIComponent(connectionId)}`,\n\t\t\t{ method: 'DELETE' },\n\t\t);\n\t\tif (!tokenRsp.ok) {\n\t\t\tconst error = await getResponseError(tokenRsp);\n\t\t\tconst errorMessage =\n\t\t\t\ttypeof error === 'string' ? error : ((error as { message?: string })?.message ?? tokenRsp.statusText);\n\t\t\tif (error != null) {\n\t\t\t\tscope?.error(undefined, `Failed to disconnect connection '${connectionId}' for ${id}: ${errorMessage}`);\n\t\t\t}\n\t\t\tthis.ctx.hooks?.connection?.onDisconnectFailed?.({ id: id, code: tokenRsp.status });\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Promotes a connection to primary by its token id (multi-account), clearing primacy on the provider's\n\t * other connections server-side. Returns whether the switch succeeded.\n\t */\n\tasync setPrimaryConnection(id: IntegrationIds, connectionId: string): Promise<boolean> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst tokenRsp = await this.ctx.account.fetchGkApi(\n\t\t\t`v1/provider-tokens/tokens/${encodeURIComponent(connectionId)}/primary`,\n\t\t\t{ method: 'POST' },\n\t\t);\n\t\tif (!tokenRsp.ok) {\n\t\t\tconst error = await getResponseError(tokenRsp);\n\t\t\tconst errorMessage =\n\t\t\t\ttypeof error === 'string' ? error : ((error as { message?: string })?.message ?? tokenRsp.statusText);\n\t\t\tif (error != null) {\n\t\t\t\tscope?.error(\n\t\t\t\t\tundefined,\n\t\t\t\t\t`Failed to set primary connection '${connectionId}' for ${id}: ${errorMessage}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cloudIntegrationService.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/cloudIntegrationService.ts"],"sourcesContent":["import { RequestRateLimitError } from '@gitlens/git/errors.js';\nimport { getScopedLogger } from '@gitlens/utils/logger.scoped.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport { isSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type {\n\tCloudIntegrationAuthenticationSession,\n\tCloudIntegrationConnection,\n\tCloudIntegrationType,\n} from './models.js';\nimport { toCloudIntegrationType } from './models.js';\n\n/**\n * Wire shape of a `v1/provider-tokens` connection descriptor. The list endpoint returns one such\n * entry per provider as the primary, with additional accounts nested under `secondaries`. Field casing\n * is the backend's (camelCase, `tokenId`).\n */\ninterface GKProviderTokenData {\n\ttokenId: string;\n\tprovider: CloudIntegrationType;\n\ttype: CloudIntegrationAuthenticationSession['type'];\n\tdomain: string;\n\t/** Human-readable account handle; the backend includes it on the connection when available. */\n\taccountName?: string;\n}\n\n/** Wire shape of a single `v1/provider-tokens/...` token response (`data`). */\ninterface GKProviderToken {\n\ttokenId?: string;\n\tisPrimary?: boolean;\n\taccessToken: string;\n\tappKey?: string;\n\texpiresIn: number;\n\tscopes: string;\n\ttype: CloudIntegrationAuthenticationSession['type'];\n\tdomain?: string;\n}\n\n/**\n * Whether the backend definitively answered that this connection has no fetchable token, so it can be\n * cleanly disconnected (#5497). Everything else is retryable — notably 401/403, which reject our GK\n * *account* token rather than the connection. The asymmetry is deliberate: a wrongly-kept descriptor\n * self-corrects on the next sync, a wrongly-dropped one needs a manual reconnect (#5569).\n */\nfunction isTerminalStatus(status: number): boolean {\n\tswitch (status) {\n\t\tcase 400: // e.g. a non-uuid token id (#5497)\n\t\tcase 404:\n\t\tcase 410:\n\t\tcase 422:\n\t\t\treturn true;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * The error for a token fetch the backend refused for a reason a retry can fix. A 429 is typed as a rate limit so\n * a read that needed the token reports a throttle rather than a broken connection (gitkraken/kepler#3546); every\n * other status stays a plain error.\n */\nfunction retryableTokenFetchError(status: number, message: string): Error {\n\tconst error = new Error(message);\n\treturn status === 429 ? new RequestRateLimitError(error, undefined, undefined) : error;\n}\n\nfunction toSession(data: GKProviderToken): CloudIntegrationAuthenticationSession {\n\t// Normalize the backend's `tokenId` onto our `id` so callers get a stable per-connection identity.\n\treturn {\n\t\ttype: data.type,\n\t\taccessToken: data.accessToken,\n\t\tdomain: data.domain ?? '',\n\t\texpiresIn: data.expiresIn,\n\t\tscopes: data.scopes,\n\t\tid: data.tokenId,\n\t\tappKey: data.appKey,\n\t};\n}\n\nasync function getResponseError(response: Response): Promise<unknown> {\n\ttry {\n\t\treturn ((await response.json()) as { error?: unknown })?.error;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport class CloudIntegrationService {\n\tconstructor(private readonly ctx: IntegrationServiceContext) {}\n\n\tasync getConnections(): Promise<CloudIntegrationConnection[] | undefined> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst providersRsp = await this.ctx.account.fetchGkApi('v1/provider-tokens', { method: 'GET' });\n\t\tif (!providersRsp.ok) {\n\t\t\tconst error = await getResponseError(providersRsp);\n\t\t\tconst errorMessage =\n\t\t\t\ttypeof error === 'string'\n\t\t\t\t\t? error\n\t\t\t\t\t: ((error as { message?: string })?.message ?? providersRsp.statusText);\n\t\t\tif (error != null) {\n\t\t\t\tscope?.error(undefined, `Failed to get connected providers from cloud: ${errorMessage}`);\n\t\t\t}\n\t\t\tthis.ctx.hooks?.connection?.onConnectionsFetchFailed?.({ code: providersRsp.status });\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst data = ((await providersRsp.json()) as { data?: unknown })?.data as\n\t\t\t| (GKProviderTokenData & { secondaries?: GKProviderTokenData[] })[]\n\t\t\t| undefined;\n\t\t// A non-array `data` (unexpected backend shape) would throw on iteration below and abort the\n\t\t// whole cloud-sync path, so bail gracefully instead.\n\t\tif (!Array.isArray(data)) return undefined;\n\n\t\t// Flatten the primary + `secondaries[]` grouping into one connection per account. Primacy is\n\t\t// positional on the wire (top-level entry = primary), so we set the `primary` flag here.\n\t\tconst connections: CloudIntegrationConnection[] = [];\n\t\tfor (const item of data) {\n\t\t\tconnections.push({\n\t\t\t\tid: item.tokenId,\n\t\t\t\ttype: item.type,\n\t\t\t\tprovider: item.provider,\n\t\t\t\tdomain: item.domain,\n\t\t\t\tprimary: true,\n\t\t\t\taccountName: item.accountName,\n\t\t\t});\n\t\t\t// `secondaries` comes from the same unknown payload as `data`; guard against a non-array shape so\n\t\t\t// an unexpected value doesn't throw here and abort the whole cloud-sync path.\n\t\t\tfor (const secondary of Array.isArray(item.secondaries) ? item.secondaries : []) {\n\t\t\t\tconnections.push({\n\t\t\t\t\tid: secondary.tokenId,\n\t\t\t\t\ttype: secondary.type,\n\t\t\t\t\tprovider: secondary.provider ?? item.provider,\n\t\t\t\t\tdomain: secondary.domain || item.domain,\n\t\t\t\t\tprimary: false,\n\t\t\t\t\taccountName: secondary.accountName,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn connections;\n\t}\n\n\t/**\n\t * Fetches a connection's token, refreshing it first when `refreshToken` is given; `connectionId` targets\n\t * a specific connection (multi-account) rather than the provider's primary.\n\t *\n\t * Resolves `undefined` ONLY on a terminal failure (see {@link isTerminalStatus}, or an ok response with\n\t * no `data`), which callers may treat as \"the connection is gone\" (#5497). THROWS on every other\n\t * failure, which they must catch and treat as retryable instead of disconnecting (#5569).\n\t */\n\tasync getConnectionSession(\n\t\tid: IntegrationIds,\n\t\trefreshToken?: string,\n\t\tconnectionId?: string,\n\t): Promise<CloudIntegrationAuthenticationSession | undefined> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst refresh = Boolean(refreshToken);\n\t\tconst cloudIntegrationType = toCloudIntegrationType[id];\n\t\tif (cloudIntegrationType == null) {\n\t\t\tscope?.error(undefined, `Unsupported cloud integration type: ${id}`);\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst reqInitOptions = refreshToken\n\t\t\t? {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\taccess_token: refreshToken,\n\t\t\t\t\t}),\n\t\t\t\t}\n\t\t\t: { method: 'GET' };\n\n\t\t// A specific connection (multi-account) is addressed by its token id under `/tokens/{tokenId}`,\n\t\t// including refresh (`/tokens/{tokenId}/refresh`). The provider-scoped endpoints only ever operate\n\t\t// on the provider's PRIMARY connection, so they must NOT be used to refresh a secondary.\n\t\tconst path = connectionId\n\t\t\t? `v1/provider-tokens/tokens/${encodeURIComponent(connectionId)}${refresh ? '/refresh' : ''}`\n\t\t\t: `v1/provider-tokens/${cloudIntegrationType}${refresh ? '/refresh' : ''}`;\n\t\tconst tokenRsp = await this.ctx.account.fetchGkApi(path, reqInitOptions);\n\t\tif (!tokenRsp.ok) {\n\t\t\tconst error = await getResponseError(tokenRsp);\n\t\t\tconst errorMessage =\n\t\t\t\ttypeof error === 'string' ? error : ((error as { message?: string })?.message ?? tokenRsp.statusText);\n\t\t\tif (error != null) {\n\t\t\t\tscope?.error(\n\t\t\t\t\tundefined,\n\t\t\t\t\t`Failed to ${refresh ? 'refresh' : 'get'} ${id} token from cloud: ${errorMessage}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthis.ctx.hooks?.connection?.onConnectionFetchFailed?.({\n\t\t\t\tid: id,\n\t\t\t\tcode: tokenRsp.status,\n\t\t\t\trefreshing: refresh,\n\t\t\t});\n\n\t\t\tif (refresh) {\n\t\t\t\t// try once to just get the latest token if the refresh fails, and give up if that fails too;\n\t\t\t\t// stay scoped to the same connection (by token id) so we don't fall back to the primary.\n\t\t\t\tconst latestPath = connectionId\n\t\t\t\t\t? `v1/provider-tokens/tokens/${encodeURIComponent(connectionId)}`\n\t\t\t\t\t: `v1/provider-tokens/${cloudIntegrationType}`;\n\t\t\t\tconst newTokenRsp = await this.ctx.account.fetchGkApi(latestPath, { method: 'GET' });\n\t\t\t\tif (newTokenRsp.ok) {\n\t\t\t\t\tconst data = ((await newTokenRsp.json()) as { data?: GKProviderToken })?.data;\n\t\t\t\t\treturn data != null ? toSession(data) : undefined;\n\t\t\t\t}\n\n\t\t\t\t// Report the fallback's own status (as a get, not a refresh) — it's what decides the outcome.\n\t\t\t\tthis.ctx.hooks?.connection?.onConnectionFetchFailed?.({\n\t\t\t\t\tid: id,\n\t\t\t\t\tcode: newTokenRsp.status,\n\t\t\t\t\trefreshing: false,\n\t\t\t\t});\n\n\t\t\t\tif (isTerminalStatus(newTokenRsp.status)) return undefined;\n\n\t\t\t\tthrow retryableTokenFetchError(\n\t\t\t\t\tnewTokenRsp.status,\n\t\t\t\t\t`Retryable failure (${newTokenRsp.status}) refreshing ${id} token from cloud`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (isTerminalStatus(tokenRsp.status)) return undefined;\n\n\t\t\tthrow retryableTokenFetchError(\n\t\t\t\ttokenRsp.status,\n\t\t\t\t`Retryable failure (${tokenRsp.status}) getting ${id} token from cloud`,\n\t\t\t);\n\t\t}\n\n\t\tconst data = ((await tokenRsp.json()) as { data?: GKProviderToken })?.data;\n\t\treturn data != null ? toSession(data) : undefined;\n\t}\n\n\tasync disconnect(id: IntegrationIds, connectionId?: string): Promise<boolean> {\n\t\tif (isSelfManagedHostIntegrationId(id)) {\n\t\t\tif (!connectionId?.trim()) return false;\n\n\t\t\treturn this.disconnectConnection(id, connectionId);\n\t\t}\n\n\t\tconst scope = getScopedLogger();\n\n\t\tconst cloudIntegrationType = toCloudIntegrationType[id];\n\t\tif (cloudIntegrationType == null) {\n\t\t\tscope?.error(undefined, `Unsupported cloud integration type: ${id}`);\n\t\t\treturn false;\n\t\t}\n\n\t\tconst tokenRsp = await this.ctx.account.fetchGkApi(`v1/provider-tokens/${cloudIntegrationType}`, {\n\t\t\tmethod: 'DELETE',\n\t\t});\n\t\tif (!tokenRsp.ok) {\n\t\t\tconst error = await getResponseError(tokenRsp);\n\t\t\tconst errorMessage =\n\t\t\t\ttypeof error === 'string' ? error : ((error as { message?: string })?.message ?? tokenRsp.statusText);\n\t\t\tif (error != null) {\n\t\t\t\tscope?.error(undefined, `Failed to disconnect ${id} token from cloud: ${errorMessage}`);\n\t\t\t}\n\t\t\tthis.ctx.hooks?.connection?.onDisconnectFailed?.({\n\t\t\t\tid: id,\n\t\t\t\tcode: tokenRsp.status,\n\t\t\t});\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Disconnects a single connection by its token id (multi-account). The backend promotes a secondary\n\t * to primary when the removed connection was the primary, so the provider stays connected if others\n\t * remain. Returns whether the delete succeeded.\n\t */\n\tasync disconnectConnection(id: IntegrationIds, connectionId: string): Promise<boolean> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst tokenRsp = await this.ctx.account.fetchGkApi(\n\t\t\t`v1/provider-tokens/tokens/${encodeURIComponent(connectionId)}`,\n\t\t\t{ method: 'DELETE' },\n\t\t);\n\t\tif (!tokenRsp.ok) {\n\t\t\tconst error = await getResponseError(tokenRsp);\n\t\t\tconst errorMessage =\n\t\t\t\ttypeof error === 'string' ? error : ((error as { message?: string })?.message ?? tokenRsp.statusText);\n\t\t\tif (error != null) {\n\t\t\t\tscope?.error(undefined, `Failed to disconnect connection '${connectionId}' for ${id}: ${errorMessage}`);\n\t\t\t}\n\t\t\tthis.ctx.hooks?.connection?.onDisconnectFailed?.({ id: id, code: tokenRsp.status });\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Promotes a connection to primary by its token id (multi-account), clearing primacy on the provider's\n\t * other connections server-side. Returns whether the switch succeeded.\n\t */\n\tasync setPrimaryConnection(id: IntegrationIds, connectionId: string): Promise<boolean> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst tokenRsp = await this.ctx.account.fetchGkApi(\n\t\t\t`v1/provider-tokens/tokens/${encodeURIComponent(connectionId)}/primary`,\n\t\t\t{ method: 'POST' },\n\t\t);\n\t\tif (!tokenRsp.ok) {\n\t\t\tconst error = await getResponseError(tokenRsp);\n\t\t\tconst errorMessage =\n\t\t\t\ttypeof error === 'string' ? error : ((error as { message?: string })?.message ?? tokenRsp.statusText);\n\t\t\tif (error != null) {\n\t\t\t\tscope?.error(\n\t\t\t\t\tundefined,\n\t\t\t\t\t`Failed to set primary connection '${connectionId}' for ${id}: ${errorMessage}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"],"mappings":"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"}
@@ -1,10 +1,10 @@
1
1
  import { flatten } from "../../../utils/iterable.js";
2
2
  import { Emitter } from "../../../utils/event.js";
3
3
  import { debounce } from "../../../utils/debounce.js";
4
- import { getSessionIdentity } from "./models.js";
5
4
  import { areDomainsOnSameHost, baseUrlFromDomain, hostFromDomain, sameConfiguredBaseUrl } from "../utils/domain.utils.js";
6
- import { isSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
7
5
  import { providersMetadata } from "../providers/models.js";
6
+ import { isSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
7
+ import { getSessionIdentity } from "./models.js";
8
8
  //#region ../plus/integrations/src/authentication/configuredIntegrationService.ts
9
9
  var ConfiguredIntegrationService = class {
10
10
  ctx;
@@ -1,9 +1,9 @@
1
1
  import { trace } from "../../../utils/decorators/log.js";
2
2
  import { Emitter } from "../../../utils/event.js";
3
3
  import __decorate from "../../../_virtual/@oxc-project+runtime/helpers/esm/decorate.js";
4
- import { isSupportedCloudIntegrationId } from "./models.js";
5
4
  import { areDomainsOnSameHost } from "../utils/domain.utils.js";
6
5
  import { isNonExpiringZeroTokenIntegrationId, isSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
6
+ import { isSupportedCloudIntegrationId } from "./models.js";
7
7
  //#region ../plus/integrations/src/authentication/integrationAuthenticationProvider.ts
8
8
  const maxSmallIntegerV8 = 2 ** 30 - 1;
9
9
  var IntegrationAuthenticationProviderBase = class {
@@ -42,10 +42,21 @@ var IntegrationAuthenticationProviderBase = class {
42
42
  let refetched;
43
43
  let signOutMark;
44
44
  if (options?.forceNewSession) {
45
+ if (isSelfManagedHostIntegrationId(this.authProviderId)) {
46
+ const connectionId = descriptor.connectionId || this.configuredIntegrationService.getConfiguredConnectionId(this.authProviderId, descriptor.domain, true);
47
+ if (!this.configuredIntegrationService.isConnectionConfiguredForHost(this.authProviderId, connectionId, descriptor.domain)) return;
48
+ descriptor = {
49
+ ...descriptor,
50
+ connectionId
51
+ };
52
+ }
45
53
  await this.configuredIntegrationService.deleteStoredSessions(this.authProviderId, {
46
54
  ...descriptor,
47
55
  cloud: true
48
- }, true, { signOut: true });
56
+ }, true, {
57
+ signOut: true,
58
+ preserveConfigured: isSelfManagedHostIntegrationId(this.authProviderId)
59
+ });
49
60
  signOutMark = this.configuredIntegrationService.getSignOutMark();
50
61
  } else {
51
62
  signOutMark = this.configuredIntegrationService.getSignOutMark();
@@ -95,7 +106,7 @@ var CloudIntegrationAuthenticationProvider = class extends IntegrationAuthentica
95
106
  }
96
107
  async getNewSession(descriptor, options) {
97
108
  if (options?.forceNewSession) {
98
- if (await this.disconnectCloudSession() === "failure") return;
109
+ if (await this.disconnectCloudSession(descriptor) === "failure") return;
99
110
  this.connectCloudSession(false, options?.source);
100
111
  return;
101
112
  }
@@ -158,9 +169,11 @@ var CloudIntegrationAuthenticationProvider = class extends IntegrationAuthentica
158
169
  appKey: session.appKey
159
170
  };
160
171
  }
161
- async disconnectCloudSession() {
172
+ async disconnectCloudSession(descriptor) {
162
173
  if (!(await this.authenticationService.ctx.account.getAccount() != null)) return "skip";
163
- return await this.authenticationService.cloudIntegrations.disconnect(this.authProviderId) ? "success" : "failure";
174
+ if (!await this.authenticationService.cloudIntegrations.disconnect(this.authProviderId, descriptor.connectionId)) return "failure";
175
+ if (isSelfManagedHostIntegrationId(this.authProviderId)) await this.deleteSession(descriptor, { preserveConfigured: false });
176
+ return "success";
164
177
  }
165
178
  };
166
179
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"integrationAuthenticationProvider.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/integrationAuthenticationProvider.ts"],"sourcesContent":["import { trace } from '@gitlens/utils/decorators/log.js';\nimport type { Disposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { Sources } from '../telemetry.js';\nimport { areDomainsOnSameHost } from '../utils/domain.utils.js';\nimport { isNonExpiringZeroTokenIntegrationId, isSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ConfiguredIntegrationService } from './configuredIntegrationService.js';\nimport type { IntegrationAuthenticationService } from './integrationAuthenticationService.js';\nimport type { ProviderAuthenticationSession } from './models.js';\nimport { isSupportedCloudIntegrationId } from './models.js';\n\nconst maxSmallIntegerV8 = 2 ** 30 - 1; // Max number that can be stored in V8's smis (small integers)\n\nexport interface IntegrationAuthenticationProviderDescriptor {\n\tid: IntegrationIds;\n\tscopes: string[];\n}\n\nexport interface IntegrationAuthenticationSessionDescriptor {\n\tdomain: string;\n\tscopes: string[];\n\t/** When set, reads/deletes only the matching storage variant. */\n\tcloud?: boolean;\n\t/**\n\t * Targets a specific connection when a provider has multiple accounts connected. When omitted,\n\t * operations resolve to the provider's primary connection (see\n\t * {@link ConfiguredIntegrationService.resolveConnectionId}).\n\t */\n\tconnectionId?: string;\n\t[key: string]: unknown;\n}\n\n/**\n * Options for {@link IntegrationAuthenticationProvider.getSession}. The two variants keep `sync` exclusive\n * with the interactive flags: a sync resolves whatever the cloud already has, so it must never open a connect\n * window.\n *\n * `refreshRejectedToken` marks the stored token as known-refused by the provider (an `AuthenticationError` on\n * a previous read), which forces a cloud `/refresh` even when the token's expiry still claims it is valid —\n * the case a purely time-based refresh cannot see. The refresh token itself never reaches the client: the GK\n * cloud performs the exchange server-side.\n *\n * `refetch` (sync only) fetches a fresh token instead of serving the stored one, WITHOUT deleting the stored one\n * first: it is replaced only once the fetch returns a session. A forced re-sync used to delete it up front, so a\n * fetch that failed transiently (a GK API 429 or 5xx) left the connection with no token at all, and every read\n * until the next good sync reported it as gone (gitkraken/kepler#3546).\n */\nexport type GetSessionOptions =\n\t| {\n\t\t\tcreateIfNeeded?: boolean;\n\t\t\tforceNewSession?: boolean;\n\t\t\tsync?: never;\n\t\t\trefetch?: never;\n\t\t\trefreshRejectedToken?: boolean;\n\t\t\tsource?: Sources;\n\t }\n\t| {\n\t\t\tcreateIfNeeded?: never;\n\t\t\tforceNewSession?: never;\n\t\t\tsync: boolean;\n\t\t\trefetch?: boolean;\n\t\t\trefreshRejectedToken?: boolean;\n\t\t\tsource?: Sources;\n\t };\n\n/**\n * What {@link IntegrationAuthenticationProviderBase.getSession} passes down once it has decided the stored\n * session can't be served as-is. `refreshIfExpired` is its own conclusion, not a caller's option.\n */\ntype GetNewSessionOptions = GetSessionOptions & { refreshIfExpired?: boolean };\n\nexport interface IntegrationAuthenticationProvider extends Disposable {\n\t/**\n\t * Clears the stored secret for one connection only (never the whole provider). Unlike\n\t * {@link deleteAllSessions}, this by default leaves the descriptor in `integrations:configured` — its\n\t * primary caller is a forced re-sync, which needs `getConfigured()` to keep reporting the connection\n\t * while a fresh session is fetched to replace the deleted secret. Implementers should not treat this as\n\t * a full disconnect. Pass `preserveConfigured: false` to also drop the descriptor (e.g. when the\n\t * re-sync's replacement fetch failed and the connection should no longer be reported as connected).\n\t */\n\tdeleteSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void>;\n\tdeleteAllSessions(descriptor?: IntegrationAuthenticationSessionDescriptor): Promise<void>;\n\t/**\n\t * Resolves the session for `descriptor`, from storage when it is still usable and from the cloud\n\t * otherwise. See {@link GetSessionOptions}.\n\t */\n\tgetSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined>;\n\tget onDidChange(): Event<void>;\n}\n\nabstract class IntegrationAuthenticationProviderBase<\n\tID extends IntegrationIds = IntegrationIds,\n> implements IntegrationAuthenticationProvider {\n\tprotected readonly disposables: Disposable[] = [];\n\n\tconstructor(\n\t\tprotected readonly authenticationService: IntegrationAuthenticationService,\n\t\tprotected readonly configuredIntegrationService: ConfiguredIntegrationService,\n\t) {}\n\n\tdispose(): void {\n\t\tthis.disposables.forEach(d => d.dispose());\n\t}\n\n\tprivate readonly _onDidChange = new Emitter<void>();\n\tget onDidChange(): Event<void> {\n\t\treturn this._onDidChange.event;\n\t}\n\n\tprotected abstract get authProviderId(): ID;\n\n\t@trace()\n\tasync deleteSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void> {\n\t\tconst domain = isSelfManagedHostIntegrationId(this.authProviderId) ? descriptor?.domain : undefined;\n\t\tconst configured = this.configuredIntegrationService.getConfigured(this.authProviderId, {\n\t\t\tdomain: domain,\n\t\t});\n\n\t\t// Scope the descriptor to cloud so resolveConnectionId targets the cloud variant's id: this is a\n\t\t// cloud-only delete, and a mixed local+cloud connection whose local descriptor is primary would\n\t\t// otherwise resolve the local id and leave the cloud secret intact.\n\t\tawait this.configuredIntegrationService.deleteStoredSessions(\n\t\t\tthis.authProviderId,\n\t\t\t{ ...descriptor, cloud: true },\n\t\t\ttrue,\n\t\t\t{ preserveConfigured: options?.preserveConfigured ?? true },\n\t\t);\n\n\t\tif (configured?.length) {\n\t\t\tthis.fireChange();\n\t\t}\n\t}\n\n\t@trace()\n\tasync deleteAllSessions(descriptor?: IntegrationAuthenticationSessionDescriptor): Promise<void> {\n\t\t// Self-managed providers group every host under one provider id, so scope the clear to this\n\t\t// descriptor's host when given; for cloud providers the domain stays undefined here, clearing every account.\n\t\tconst domain = isSelfManagedHostIntegrationId(this.authProviderId) ? descriptor?.domain : undefined;\n\t\tconst configured = this.configuredIntegrationService.getConfigured(this.authProviderId, {\n\t\t\tdomain: domain,\n\t\t});\n\n\t\tawait this.configuredIntegrationService.deleteAllStoredSessions(this.authProviderId, undefined, domain);\n\n\t\tif (configured?.length) {\n\t\t\tthis.fireChange();\n\t\t}\n\t}\n\n\t@trace()\n\tasync getSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tlet session;\n\t\tlet previousToken;\n\t\tlet refetched: ProviderAuthenticationSession | undefined;\n\t\tlet signOutMark: number;\n\t\tif (options?.forceNewSession) {\n\t\t\t// Cloud-only delete (see deleteSession): scope to cloud so the cloud variant's id is cleared even\n\t\t\t// when a mixed local+cloud connection's local descriptor is primary. A sign-out, so a token fetched before\n\t\t\t// it is not stored back; the mark is taken after it, so this session's own fetch is not refused.\n\t\t\tawait this.configuredIntegrationService.deleteStoredSessions(\n\t\t\t\tthis.authProviderId,\n\t\t\t\t{ ...descriptor, cloud: true },\n\t\t\t\ttrue,\n\t\t\t\t{ signOut: true },\n\t\t\t);\n\t\t\tsignOutMark = this.configuredIntegrationService.getSignOutMark();\n\t\t} else {\n\t\t\t// Taken before the stored-token read (a sign-out landing during the read is ordered after it by the storage\n\t\t\t// lock, so it counts either way).\n\t\t\tsignOutMark = this.configuredIntegrationService.getSignOutMark();\n\t\t\tconst stored = await this.configuredIntegrationService.getStoredSession(\n\t\t\t\tthis.authProviderId,\n\t\t\t\toptions?.sync ? { ...descriptor, cloud: true } : descriptor,\n\t\t\t);\n\t\t\tpreviousToken = stored?.accessToken;\n\t\t\tif (options?.sync && options.refetch) {\n\t\t\t\trefetched = stored;\n\t\t\t} else {\n\t\t\t\tsession = stored;\n\t\t\t}\n\t\t}\n\n\t\tconst isExpiredSession = session?.expiresAt != null && new Date(session.expiresAt).getTime() < Date.now();\n\t\t// A rejected token has to re-resolve even when the stored session looks perfectly healthy: that is\n\t\t// the whole point — the provider refused it while its expiry still says valid. Without this the\n\t\t// stored session short-circuits here and the refresh below is never reached.\n\t\tconst refreshRejectedToken = options?.refreshRejectedToken === true && session != null;\n\t\tif (session == null || isExpiredSession || refreshRejectedToken) {\n\t\t\tsession = await this.getNewSession(descriptor, {\n\t\t\t\t...options,\n\t\t\t\trefreshIfExpired: isExpiredSession,\n\t\t\t\trefreshRejectedToken: refreshRejectedToken,\n\t\t\t});\n\t\t\t// Stored only when no sign-out landed since the mark (checked and written under the storage lock): a token\n\t\t\t// read or fetched across a disconnect, a reauthentication or the connection's removal is not reconnected.\n\t\t\tif (\n\t\t\t\tsession != null &&\n\t\t\t\t!(await this.configuredIntegrationService.storeSession(this.authProviderId, session, {\n\t\t\t\t\tsignOutMark: signOutMark,\n\t\t\t\t}))\n\t\t\t) {\n\t\t\t\tsession = undefined;\n\t\t\t}\n\n\t\t\tif (session != null) {\n\t\t\t\t// The replacement belongs to a different connection (the cloud's primary moved to another\n\t\t\t\t// account), so it was stored beside the one it replaces rather than over it. Drop the old token\n\t\t\t\t// as the up-front delete used to, or an unscoped read would keep resolving it until the next sync.\n\t\t\t\tif (refetched?.id != null && refetched.id !== session.id) {\n\t\t\t\t\tawait this.configuredIntegrationService.deleteStoredSessions(\n\t\t\t\t\t\tthis.authProviderId,\n\t\t\t\t\t\t{ ...descriptor, connectionId: refetched.id, cloud: true },\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t{ preserveConfigured: true },\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (previousToken !== session?.accessToken) {\n\t\t\tthis.fireChange();\n\t\t}\n\n\t\treturn session;\n\t}\n\n\tprotected abstract getNewSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetNewSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined>;\n\n\tprotected fireChange(): void {\n\t\tqueueMicrotask(() => this._onDidChange.fire());\n\t}\n}\n\nexport class CloudIntegrationAuthenticationProvider<\n\tID extends IntegrationIds = IntegrationIds,\n> extends IntegrationAuthenticationProviderBase<ID> {\n\tconstructor(\n\t\tauthenticationService: IntegrationAuthenticationService,\n\t\tconfiguredIntegrationService: ConfiguredIntegrationService,\n\t\tprivate readonly _authProviderId: ID,\n\t) {\n\t\tsuper(authenticationService, configuredIntegrationService);\n\t}\n\n\tprotected get authProviderId(): ID {\n\t\treturn this._authProviderId;\n\t}\n\n\tprotected override async getNewSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetNewSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tif (options?.forceNewSession) {\n\t\t\tif ((await this.disconnectCloudSession()) === 'failure') {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tvoid this.connectCloudSession(false, options?.source);\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// TODO: This is a stopgap to make sure we're not hammering the api on automatic calls to get the session.\n\t\t// Ultimately we want to timestamp calls to syncCloudIntegrations and use that to determine whether we should\n\t\t// make the call or not.\n\t\t// `refreshRejectedToken` joins the list for the same reason `refreshIfExpired` is on it: both mean the\n\t\t// session in hand is known-unusable, so skipping the cloud round trip would just re-serve it.\n\t\tlet session =\n\t\t\toptions?.refreshIfExpired ||\n\t\t\toptions?.refreshRejectedToken ||\n\t\t\toptions?.createIfNeeded ||\n\t\t\toptions?.forceNewSession ||\n\t\t\toptions?.sync\n\t\t\t\t? await this.getCloudSession(descriptor, { refreshRejectedToken: options?.refreshRejectedToken })\n\t\t\t\t: undefined;\n\n\t\tconst shouldCreateSession = options?.createIfNeeded && session == null;\n\t\tif (shouldCreateSession) {\n\t\t\tconst connected = await this.connectCloudSession(true, options?.source);\n\t\t\tif (!connected) return undefined;\n\n\t\t\t// This should get us the session we just created with connectCloudSession, because a syncCloudIntegrations run from\n\t\t\t// integration service should have resulted in it being created and stored by this provider\n\t\t\tsession = await this.getSession(descriptor, { source: options?.source });\n\t\t}\n\t\treturn session;\n\t}\n\n\tprivate connectCloudSession(skipIfConnected: boolean, source: Sources | undefined): Promise<boolean> {\n\t\t// Capture in a const so the type guard narrows it to `SupportedCloudIntegrationIds` (a getter\n\t\t// access wouldn't narrow), letting us call the service directly without a cast.\n\t\tconst id = this.authProviderId;\n\t\tif (isSupportedCloudIntegrationId(id)) {\n\t\t\treturn this.authenticationService.connectCloudIntegrations(\n\t\t\t\t{ integrationIds: [id], skipIfConnected: skipIfConnected, skipPreSync: true },\n\t\t\t\t{\n\t\t\t\t\tsource: source ?? 'integrations',\n\t\t\t\t\tdetail: {\n\t\t\t\t\t\taction: 'connect',\n\t\t\t\t\t\tintegration: id,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\n\t\treturn Promise.resolve(false);\n\t}\n\n\tprivate async getCloudSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: { refreshRejectedToken?: boolean },\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tconst loggedIn = (await this.authenticationService.ctx.account.getAccount()) != null;\n\t\tif (!loggedIn) return undefined;\n\n\t\tconst cloudIntegrations = this.authenticationService.cloudIntegrations;\n\t\t// An unscoped descriptor (no explicit connectionId) would fetch the provider-global primary via\n\t\t// `v1/provider-tokens/<provider>`. For a self-managed provider spanning multiple hosts that primary\n\t\t// can belong to a different host, so a forced sync of host A would hydrate host B's token here (before\n\t\t// reconcile corrects storage). Scope to this host's own configured connection when we have one; fall\n\t\t// through to the provider-scoped path only when nothing is configured yet (legacy/first sync).\n\t\tconst connectionId =\n\t\t\tdescriptor.connectionId ??\n\t\t\t(isSelfManagedHostIntegrationId(this.authProviderId)\n\t\t\t\t? this.configuredIntegrationService.getConfiguredConnectionId(\n\t\t\t\t\t\tthis.authProviderId,\n\t\t\t\t\t\tdescriptor.domain,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t)\n\t\t\t\t: undefined);\n\t\t// Refuses a session issued for a DIFFERENT host than the one this connection is keyed by, which would\n\t\t// otherwise let host B's token serve host A.\n\t\t//\n\t\t// A session that names no host is accepted ONLY when it was fetched by a connection id WE have stored\n\t\t// for this host. That descriptor is what ties the token to the host, so an absent domain then means the\n\t\t// backend did not say rather than that the token belongs elsewhere, and refusing it would strand the\n\t\t// refresh.\n\t\t//\n\t\t// A non-null id is not enough on its own: `descriptor.connectionId` can come from the caller, and with\n\t\t// no id at all the fetch goes to the provider-GLOBAL primary endpoint whose token can belong to any\n\t\t// configured host. In both of those cases a domainless response must be refused, or a token would be\n\t\t// bound to — and sent to — a host it was never issued for.\n\t\tconst scopedToThisHost = this.configuredIntegrationService.isConnectionConfiguredForHost(\n\t\t\tthis.authProviderId,\n\t\t\tconnectionId,\n\t\t\tdescriptor.domain,\n\t\t);\n\t\tconst isForAnotherHost = (session: { domain: string } | undefined) =>\n\t\t\tsession != null &&\n\t\t\tisSelfManagedHostIntegrationId(this.authProviderId) &&\n\t\t\t(!scopedToThisHost || Boolean(session.domain?.trim())) &&\n\t\t\t!areDomainsOnSameHost(session.domain, descriptor.domain);\n\n\t\tlet session = await cloudIntegrations.getConnectionSession(this.authProviderId, undefined, connectionId);\n\t\tif (isForAnotherHost(session)) return undefined;\n\n\t\t// GitHub, the cloud self-managed hosts, and Trello return `expiresIn: 0` for a token that never\n\t\t// expires; left as 0 the session would be built with `expiresAt = now` and rejected as expired on the\n\t\t// next resolution. Map it to the maximum expiry so it isn't refreshed frequently.\n\t\tif (session?.expiresIn === 0 && isNonExpiringZeroTokenIntegrationId(this.authProviderId)) {\n\t\t\tsession.expiresIn = maxSmallIntegerV8; // maximum expiration length\n\t\t}\n\n\t\t// `expiresIn < 60` is a PREDICTION that the token is about to lapse, and it is the only trigger the\n\t\t// refresh used to have. It cannot see a token the provider has already refused while the backend\n\t\t// still believes it is valid — a revoked grant, retired scopes, an app removed from the org — where\n\t\t// `expiresIn` stays high and the doomed token is re-sent on every read. `refreshRejectedToken` is\n\t\t// the observed counterpart: the caller saw the provider reject this exact token, so refresh it\n\t\t// regardless of what its expiry claims.\n\t\tif (session != null && (session.expiresIn < 60 || options?.refreshRejectedToken)) {\n\t\t\tsession = await cloudIntegrations.getConnectionSession(\n\t\t\t\tthis.authProviderId,\n\t\t\t\tsession.accessToken,\n\t\t\t\tconnectionId,\n\t\t\t);\n\t\t\tif (isForAnotherHost(session)) return undefined;\n\t\t}\n\n\t\tif (!session) return undefined;\n\n\t\tlet sessionProtocol;\n\t\t// Only derive a protocol from a domain carrying an explicit scheme; a bare `host:port` (e.g. a\n\t\t// self-managed `ghe.example.com:8443`) parses the host as the protocol, corrupting the value.\n\t\tif (/^[a-z][a-z\\d+\\-.]*:\\/\\//i.test(session.domain)) {\n\t\t\ttry {\n\t\t\t\tsessionProtocol = new URL(session.domain).protocol;\n\t\t\t} catch {\n\t\t\t\tsessionProtocol = undefined;\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\t// Prefer the backend's per-connection token id (multi-account); fall back to the resolved\n\t\t\t// primary/legacy connection id so existing single-connection storage keys are preserved.\n\t\t\tid: session.id ?? this.configuredIntegrationService.resolveConnectionId(this.authProviderId, descriptor),\n\t\t\taccessToken: session.accessToken,\n\t\t\tscopes: descriptor.scopes,\n\t\t\taccount: {\n\t\t\t\tid: '',\n\t\t\t\tlabel: '',\n\t\t\t},\n\t\t\tcloud: true,\n\t\t\ttype: session.type,\n\t\t\texpiresAt: new Date(session.expiresIn * 1000 + Date.now()),\n\t\t\t// Note: do not use the session's domain, because the format is different than in our model\n\t\t\tdomain: descriptor.domain,\n\t\t\t// That format difference is exactly what `baseUrl` is for: the descriptor's domain is the\n\t\t\t// normalized host this connection is keyed by, so a context path only survives on the wire value.\n\t\t\t// When the backend reports no domain at all, KEEP the address already configured rather than\n\t\t\t// writing `undefined` — `writeSecret` mirrors this onto the descriptor, so falling back to the\n\t\t\t// bare host here would drop a context path the user never changed on the next refresh.\n\t\t\tbaseUrl:\n\t\t\t\tsession.domain ||\n\t\t\t\tthis.configuredIntegrationService.getConfiguredBaseUrl(\n\t\t\t\t\tthis.authProviderId,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\tdescriptor.domain,\n\t\t\t\t),\n\t\t\tprotocol: sessionProtocol ?? undefined,\n\t\t\t// Carried for providers whose client needs an app key alongside the token (e.g. Trello).\n\t\t\tappKey: session.appKey,\n\t\t};\n\t}\n\n\tprivate async disconnectCloudSession(): Promise<'skip' | 'success' | 'failure'> {\n\t\tconst loggedIn = (await this.authenticationService.ctx.account.getAccount()) != null;\n\t\tif (!loggedIn) return 'skip';\n\n\t\tconst cloudIntegrations = this.authenticationService.cloudIntegrations;\n\t\treturn (await cloudIntegrations.disconnect(this.authProviderId)) ? 'success' : 'failure';\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"integrationAuthenticationProvider.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/integrationAuthenticationProvider.ts"],"sourcesContent":["import { trace } from '@gitlens/utils/decorators/log.js';\nimport type { Disposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { Sources } from '../telemetry.js';\nimport { areDomainsOnSameHost } from '../utils/domain.utils.js';\nimport { isNonExpiringZeroTokenIntegrationId, isSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ConfiguredIntegrationService } from './configuredIntegrationService.js';\nimport type { IntegrationAuthenticationService } from './integrationAuthenticationService.js';\nimport type { ProviderAuthenticationSession } from './models.js';\nimport { isSupportedCloudIntegrationId } from './models.js';\n\nconst maxSmallIntegerV8 = 2 ** 30 - 1; // Max number that can be stored in V8's smis (small integers)\n\nexport interface IntegrationAuthenticationProviderDescriptor {\n\tid: IntegrationIds;\n\tscopes: string[];\n}\n\nexport interface IntegrationAuthenticationSessionDescriptor {\n\tdomain: string;\n\tscopes: string[];\n\t/** When set, reads/deletes only the matching storage variant. */\n\tcloud?: boolean;\n\t/**\n\t * Targets a specific connection when a provider has multiple accounts connected. When omitted,\n\t * operations resolve to the provider's primary connection (see\n\t * {@link ConfiguredIntegrationService.resolveConnectionId}).\n\t */\n\tconnectionId?: string;\n\t[key: string]: unknown;\n}\n\n/**\n * Options for {@link IntegrationAuthenticationProvider.getSession}. The two variants keep `sync` exclusive\n * with the interactive flags: a sync resolves whatever the cloud already has, so it must never open a connect\n * window.\n *\n * `refreshRejectedToken` marks the stored token as known-refused by the provider (an `AuthenticationError` on\n * a previous read), which forces a cloud `/refresh` even when the token's expiry still claims it is valid —\n * the case a purely time-based refresh cannot see. The refresh token itself never reaches the client: the GK\n * cloud performs the exchange server-side.\n *\n * `refetch` (sync only) fetches a fresh token instead of serving the stored one, WITHOUT deleting the stored one\n * first: it is replaced only once the fetch returns a session. A forced re-sync used to delete it up front, so a\n * fetch that failed transiently (a GK API 429 or 5xx) left the connection with no token at all, and every read\n * until the next good sync reported it as gone (gitkraken/kepler#3546).\n */\nexport type GetSessionOptions =\n\t| {\n\t\t\tcreateIfNeeded?: boolean;\n\t\t\tforceNewSession?: boolean;\n\t\t\tsync?: never;\n\t\t\trefetch?: never;\n\t\t\trefreshRejectedToken?: boolean;\n\t\t\tsource?: Sources;\n\t }\n\t| {\n\t\t\tcreateIfNeeded?: never;\n\t\t\tforceNewSession?: never;\n\t\t\tsync: boolean;\n\t\t\trefetch?: boolean;\n\t\t\trefreshRejectedToken?: boolean;\n\t\t\tsource?: Sources;\n\t };\n\n/**\n * What {@link IntegrationAuthenticationProviderBase.getSession} passes down once it has decided the stored\n * session can't be served as-is. `refreshIfExpired` is its own conclusion, not a caller's option.\n */\ntype GetNewSessionOptions = GetSessionOptions & { refreshIfExpired?: boolean };\n\nexport interface IntegrationAuthenticationProvider extends Disposable {\n\t/**\n\t * Clears the stored secret for one connection only (never the whole provider). Unlike\n\t * {@link deleteAllSessions}, this by default leaves the descriptor in `integrations:configured` — its\n\t * primary caller is a forced re-sync, which needs `getConfigured()` to keep reporting the connection\n\t * while a fresh session is fetched to replace the deleted secret. Implementers should not treat this as\n\t * a full disconnect. Pass `preserveConfigured: false` to also drop the descriptor (e.g. when the\n\t * re-sync's replacement fetch failed and the connection should no longer be reported as connected).\n\t */\n\tdeleteSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void>;\n\tdeleteAllSessions(descriptor?: IntegrationAuthenticationSessionDescriptor): Promise<void>;\n\t/**\n\t * Resolves the session for `descriptor`, from storage when it is still usable and from the cloud\n\t * otherwise. See {@link GetSessionOptions}.\n\t */\n\tgetSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined>;\n\tget onDidChange(): Event<void>;\n}\n\nabstract class IntegrationAuthenticationProviderBase<\n\tID extends IntegrationIds = IntegrationIds,\n> implements IntegrationAuthenticationProvider {\n\tprotected readonly disposables: Disposable[] = [];\n\n\tconstructor(\n\t\tprotected readonly authenticationService: IntegrationAuthenticationService,\n\t\tprotected readonly configuredIntegrationService: ConfiguredIntegrationService,\n\t) {}\n\n\tdispose(): void {\n\t\tthis.disposables.forEach(d => d.dispose());\n\t}\n\n\tprivate readonly _onDidChange = new Emitter<void>();\n\tget onDidChange(): Event<void> {\n\t\treturn this._onDidChange.event;\n\t}\n\n\tprotected abstract get authProviderId(): ID;\n\n\t@trace()\n\tasync deleteSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void> {\n\t\tconst domain = isSelfManagedHostIntegrationId(this.authProviderId) ? descriptor?.domain : undefined;\n\t\tconst configured = this.configuredIntegrationService.getConfigured(this.authProviderId, {\n\t\t\tdomain: domain,\n\t\t});\n\n\t\t// Scope the descriptor to cloud so resolveConnectionId targets the cloud variant's id: this is a\n\t\t// cloud-only delete, and a mixed local+cloud connection whose local descriptor is primary would\n\t\t// otherwise resolve the local id and leave the cloud secret intact.\n\t\tawait this.configuredIntegrationService.deleteStoredSessions(\n\t\t\tthis.authProviderId,\n\t\t\t{ ...descriptor, cloud: true },\n\t\t\ttrue,\n\t\t\t{ preserveConfigured: options?.preserveConfigured ?? true },\n\t\t);\n\n\t\tif (configured?.length) {\n\t\t\tthis.fireChange();\n\t\t}\n\t}\n\n\t@trace()\n\tasync deleteAllSessions(descriptor?: IntegrationAuthenticationSessionDescriptor): Promise<void> {\n\t\t// Self-managed providers group every host under one provider id, so scope the clear to this\n\t\t// descriptor's host when given; for cloud providers the domain stays undefined here, clearing every account.\n\t\tconst domain = isSelfManagedHostIntegrationId(this.authProviderId) ? descriptor?.domain : undefined;\n\t\tconst configured = this.configuredIntegrationService.getConfigured(this.authProviderId, {\n\t\t\tdomain: domain,\n\t\t});\n\n\t\tawait this.configuredIntegrationService.deleteAllStoredSessions(this.authProviderId, undefined, domain);\n\n\t\tif (configured?.length) {\n\t\t\tthis.fireChange();\n\t\t}\n\t}\n\n\t@trace()\n\tasync getSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tlet session;\n\t\tlet previousToken;\n\t\tlet refetched: ProviderAuthenticationSession | undefined;\n\t\tlet signOutMark: number;\n\t\tif (options?.forceNewSession) {\n\t\t\tif (isSelfManagedHostIntegrationId(this.authProviderId)) {\n\t\t\t\tconst connectionId =\n\t\t\t\t\tdescriptor.connectionId ||\n\t\t\t\t\tthis.configuredIntegrationService.getConfiguredConnectionId(\n\t\t\t\t\t\tthis.authProviderId,\n\t\t\t\t\t\tdescriptor.domain,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t);\n\t\t\t\tif (\n\t\t\t\t\t!this.configuredIntegrationService.isConnectionConfiguredForHost(\n\t\t\t\t\t\tthis.authProviderId,\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t\tdescriptor.domain,\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\n\t\t\t\tdescriptor = { ...descriptor, connectionId: connectionId };\n\t\t\t}\n\t\t\t// Cloud-only delete (see deleteSession): scope to cloud so the cloud variant's id is cleared even\n\t\t\t// when a mixed local+cloud connection's local descriptor is primary. A sign-out, so a token fetched before\n\t\t\t// it is not stored back; the mark is taken after it, so this session's own fetch is not refused.\n\t\t\tawait this.configuredIntegrationService.deleteStoredSessions(\n\t\t\t\tthis.authProviderId,\n\t\t\t\t{ ...descriptor, cloud: true },\n\t\t\t\ttrue,\n\t\t\t\t{ signOut: true, preserveConfigured: isSelfManagedHostIntegrationId(this.authProviderId) },\n\t\t\t);\n\t\t\tsignOutMark = this.configuredIntegrationService.getSignOutMark();\n\t\t} else {\n\t\t\t// Taken before the stored-token read (a sign-out landing during the read is ordered after it by the storage\n\t\t\t// lock, so it counts either way).\n\t\t\tsignOutMark = this.configuredIntegrationService.getSignOutMark();\n\t\t\tconst stored = await this.configuredIntegrationService.getStoredSession(\n\t\t\t\tthis.authProviderId,\n\t\t\t\toptions?.sync ? { ...descriptor, cloud: true } : descriptor,\n\t\t\t);\n\t\t\tpreviousToken = stored?.accessToken;\n\t\t\tif (options?.sync && options.refetch) {\n\t\t\t\trefetched = stored;\n\t\t\t} else {\n\t\t\t\tsession = stored;\n\t\t\t}\n\t\t}\n\n\t\tconst isExpiredSession = session?.expiresAt != null && new Date(session.expiresAt).getTime() < Date.now();\n\t\t// A rejected token has to re-resolve even when the stored session looks perfectly healthy: that is\n\t\t// the whole point — the provider refused it while its expiry still says valid. Without this the\n\t\t// stored session short-circuits here and the refresh below is never reached.\n\t\tconst refreshRejectedToken = options?.refreshRejectedToken === true && session != null;\n\t\tif (session == null || isExpiredSession || refreshRejectedToken) {\n\t\t\tsession = await this.getNewSession(descriptor, {\n\t\t\t\t...options,\n\t\t\t\trefreshIfExpired: isExpiredSession,\n\t\t\t\trefreshRejectedToken: refreshRejectedToken,\n\t\t\t});\n\t\t\t// Stored only when no sign-out landed since the mark (checked and written under the storage lock): a token\n\t\t\t// read or fetched across a disconnect, a reauthentication or the connection's removal is not reconnected.\n\t\t\tif (\n\t\t\t\tsession != null &&\n\t\t\t\t!(await this.configuredIntegrationService.storeSession(this.authProviderId, session, {\n\t\t\t\t\tsignOutMark: signOutMark,\n\t\t\t\t}))\n\t\t\t) {\n\t\t\t\tsession = undefined;\n\t\t\t}\n\n\t\t\tif (session != null) {\n\t\t\t\t// The replacement belongs to a different connection (the cloud's primary moved to another\n\t\t\t\t// account), so it was stored beside the one it replaces rather than over it. Drop the old token\n\t\t\t\t// as the up-front delete used to, or an unscoped read would keep resolving it until the next sync.\n\t\t\t\tif (refetched?.id != null && refetched.id !== session.id) {\n\t\t\t\t\tawait this.configuredIntegrationService.deleteStoredSessions(\n\t\t\t\t\t\tthis.authProviderId,\n\t\t\t\t\t\t{ ...descriptor, connectionId: refetched.id, cloud: true },\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t{ preserveConfigured: true },\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (previousToken !== session?.accessToken) {\n\t\t\tthis.fireChange();\n\t\t}\n\n\t\treturn session;\n\t}\n\n\tprotected abstract getNewSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetNewSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined>;\n\n\tprotected fireChange(): void {\n\t\tqueueMicrotask(() => this._onDidChange.fire());\n\t}\n}\n\nexport class CloudIntegrationAuthenticationProvider<\n\tID extends IntegrationIds = IntegrationIds,\n> extends IntegrationAuthenticationProviderBase<ID> {\n\tconstructor(\n\t\tauthenticationService: IntegrationAuthenticationService,\n\t\tconfiguredIntegrationService: ConfiguredIntegrationService,\n\t\tprivate readonly _authProviderId: ID,\n\t) {\n\t\tsuper(authenticationService, configuredIntegrationService);\n\t}\n\n\tprotected get authProviderId(): ID {\n\t\treturn this._authProviderId;\n\t}\n\n\tprotected override async getNewSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetNewSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tif (options?.forceNewSession) {\n\t\t\tif ((await this.disconnectCloudSession(descriptor)) === 'failure') {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tvoid this.connectCloudSession(false, options?.source);\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// TODO: This is a stopgap to make sure we're not hammering the api on automatic calls to get the session.\n\t\t// Ultimately we want to timestamp calls to syncCloudIntegrations and use that to determine whether we should\n\t\t// make the call or not.\n\t\t// `refreshRejectedToken` joins the list for the same reason `refreshIfExpired` is on it: both mean the\n\t\t// session in hand is known-unusable, so skipping the cloud round trip would just re-serve it.\n\t\tlet session =\n\t\t\toptions?.refreshIfExpired ||\n\t\t\toptions?.refreshRejectedToken ||\n\t\t\toptions?.createIfNeeded ||\n\t\t\toptions?.forceNewSession ||\n\t\t\toptions?.sync\n\t\t\t\t? await this.getCloudSession(descriptor, { refreshRejectedToken: options?.refreshRejectedToken })\n\t\t\t\t: undefined;\n\n\t\tconst shouldCreateSession = options?.createIfNeeded && session == null;\n\t\tif (shouldCreateSession) {\n\t\t\tconst connected = await this.connectCloudSession(true, options?.source);\n\t\t\tif (!connected) return undefined;\n\n\t\t\t// This should get us the session we just created with connectCloudSession, because a syncCloudIntegrations run from\n\t\t\t// integration service should have resulted in it being created and stored by this provider\n\t\t\tsession = await this.getSession(descriptor, { source: options?.source });\n\t\t}\n\t\treturn session;\n\t}\n\n\tprivate connectCloudSession(skipIfConnected: boolean, source: Sources | undefined): Promise<boolean> {\n\t\t// Capture in a const so the type guard narrows it to `SupportedCloudIntegrationIds` (a getter\n\t\t// access wouldn't narrow), letting us call the service directly without a cast.\n\t\tconst id = this.authProviderId;\n\t\tif (isSupportedCloudIntegrationId(id)) {\n\t\t\treturn this.authenticationService.connectCloudIntegrations(\n\t\t\t\t{ integrationIds: [id], skipIfConnected: skipIfConnected, skipPreSync: true },\n\t\t\t\t{\n\t\t\t\t\tsource: source ?? 'integrations',\n\t\t\t\t\tdetail: {\n\t\t\t\t\t\taction: 'connect',\n\t\t\t\t\t\tintegration: id,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\n\t\treturn Promise.resolve(false);\n\t}\n\n\tprivate async getCloudSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: { refreshRejectedToken?: boolean },\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tconst loggedIn = (await this.authenticationService.ctx.account.getAccount()) != null;\n\t\tif (!loggedIn) return undefined;\n\n\t\tconst cloudIntegrations = this.authenticationService.cloudIntegrations;\n\t\t// An unscoped descriptor (no explicit connectionId) would fetch the provider-global primary via\n\t\t// `v1/provider-tokens/<provider>`. For a self-managed provider spanning multiple hosts that primary\n\t\t// can belong to a different host, so a forced sync of host A would hydrate host B's token here (before\n\t\t// reconcile corrects storage). Scope to this host's own configured connection when we have one; fall\n\t\t// through to the provider-scoped path only when nothing is configured yet (legacy/first sync).\n\t\tconst connectionId =\n\t\t\tdescriptor.connectionId ??\n\t\t\t(isSelfManagedHostIntegrationId(this.authProviderId)\n\t\t\t\t? this.configuredIntegrationService.getConfiguredConnectionId(\n\t\t\t\t\t\tthis.authProviderId,\n\t\t\t\t\t\tdescriptor.domain,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t)\n\t\t\t\t: undefined);\n\t\t// Refuses a session issued for a DIFFERENT host than the one this connection is keyed by, which would\n\t\t// otherwise let host B's token serve host A.\n\t\t//\n\t\t// A session that names no host is accepted ONLY when it was fetched by a connection id WE have stored\n\t\t// for this host. That descriptor is what ties the token to the host, so an absent domain then means the\n\t\t// backend did not say rather than that the token belongs elsewhere, and refusing it would strand the\n\t\t// refresh.\n\t\t//\n\t\t// A non-null id is not enough on its own: `descriptor.connectionId` can come from the caller, and with\n\t\t// no id at all the fetch goes to the provider-GLOBAL primary endpoint whose token can belong to any\n\t\t// configured host. In both of those cases a domainless response must be refused, or a token would be\n\t\t// bound to — and sent to — a host it was never issued for.\n\t\tconst scopedToThisHost = this.configuredIntegrationService.isConnectionConfiguredForHost(\n\t\t\tthis.authProviderId,\n\t\t\tconnectionId,\n\t\t\tdescriptor.domain,\n\t\t);\n\t\tconst isForAnotherHost = (session: { domain: string } | undefined) =>\n\t\t\tsession != null &&\n\t\t\tisSelfManagedHostIntegrationId(this.authProviderId) &&\n\t\t\t(!scopedToThisHost || Boolean(session.domain?.trim())) &&\n\t\t\t!areDomainsOnSameHost(session.domain, descriptor.domain);\n\n\t\tlet session = await cloudIntegrations.getConnectionSession(this.authProviderId, undefined, connectionId);\n\t\tif (isForAnotherHost(session)) return undefined;\n\n\t\t// GitHub, the cloud self-managed hosts, and Trello return `expiresIn: 0` for a token that never\n\t\t// expires; left as 0 the session would be built with `expiresAt = now` and rejected as expired on the\n\t\t// next resolution. Map it to the maximum expiry so it isn't refreshed frequently.\n\t\tif (session?.expiresIn === 0 && isNonExpiringZeroTokenIntegrationId(this.authProviderId)) {\n\t\t\tsession.expiresIn = maxSmallIntegerV8; // maximum expiration length\n\t\t}\n\n\t\t// `expiresIn < 60` is a PREDICTION that the token is about to lapse, and it is the only trigger the\n\t\t// refresh used to have. It cannot see a token the provider has already refused while the backend\n\t\t// still believes it is valid — a revoked grant, retired scopes, an app removed from the org — where\n\t\t// `expiresIn` stays high and the doomed token is re-sent on every read. `refreshRejectedToken` is\n\t\t// the observed counterpart: the caller saw the provider reject this exact token, so refresh it\n\t\t// regardless of what its expiry claims.\n\t\tif (session != null && (session.expiresIn < 60 || options?.refreshRejectedToken)) {\n\t\t\tsession = await cloudIntegrations.getConnectionSession(\n\t\t\t\tthis.authProviderId,\n\t\t\t\tsession.accessToken,\n\t\t\t\tconnectionId,\n\t\t\t);\n\t\t\tif (isForAnotherHost(session)) return undefined;\n\t\t}\n\n\t\tif (!session) return undefined;\n\n\t\tlet sessionProtocol;\n\t\t// Only derive a protocol from a domain carrying an explicit scheme; a bare `host:port` (e.g. a\n\t\t// self-managed `ghe.example.com:8443`) parses the host as the protocol, corrupting the value.\n\t\tif (/^[a-z][a-z\\d+\\-.]*:\\/\\//i.test(session.domain)) {\n\t\t\ttry {\n\t\t\t\tsessionProtocol = new URL(session.domain).protocol;\n\t\t\t} catch {\n\t\t\t\tsessionProtocol = undefined;\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\t// Prefer the backend's per-connection token id (multi-account); fall back to the resolved\n\t\t\t// primary/legacy connection id so existing single-connection storage keys are preserved.\n\t\t\tid: session.id ?? this.configuredIntegrationService.resolveConnectionId(this.authProviderId, descriptor),\n\t\t\taccessToken: session.accessToken,\n\t\t\tscopes: descriptor.scopes,\n\t\t\taccount: {\n\t\t\t\tid: '',\n\t\t\t\tlabel: '',\n\t\t\t},\n\t\t\tcloud: true,\n\t\t\ttype: session.type,\n\t\t\texpiresAt: new Date(session.expiresIn * 1000 + Date.now()),\n\t\t\t// Note: do not use the session's domain, because the format is different than in our model\n\t\t\tdomain: descriptor.domain,\n\t\t\t// That format difference is exactly what `baseUrl` is for: the descriptor's domain is the\n\t\t\t// normalized host this connection is keyed by, so a context path only survives on the wire value.\n\t\t\t// When the backend reports no domain at all, KEEP the address already configured rather than\n\t\t\t// writing `undefined` — `writeSecret` mirrors this onto the descriptor, so falling back to the\n\t\t\t// bare host here would drop a context path the user never changed on the next refresh.\n\t\t\tbaseUrl:\n\t\t\t\tsession.domain ||\n\t\t\t\tthis.configuredIntegrationService.getConfiguredBaseUrl(\n\t\t\t\t\tthis.authProviderId,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\tdescriptor.domain,\n\t\t\t\t),\n\t\t\tprotocol: sessionProtocol ?? undefined,\n\t\t\t// Carried for providers whose client needs an app key alongside the token (e.g. Trello).\n\t\t\tappKey: session.appKey,\n\t\t};\n\t}\n\n\tprivate async disconnectCloudSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t): Promise<'skip' | 'success' | 'failure'> {\n\t\tconst loggedIn = (await this.authenticationService.ctx.account.getAccount()) != null;\n\t\tif (!loggedIn) return 'skip';\n\n\t\tconst cloudIntegrations = this.authenticationService.cloudIntegrations;\n\t\tif (!(await cloudIntegrations.disconnect(this.authProviderId, descriptor.connectionId))) return 'failure';\n\n\t\tif (isSelfManagedHostIntegrationId(this.authProviderId)) {\n\t\t\tawait this.deleteSession(descriptor, { preserveConfigured: false });\n\t\t}\n\n\t\treturn 'success';\n\t}\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ import { ConfiguredIntegrationService } from "./configuredIntegrationService.js"
6
6
  import { GitHostIntegration } from "../models/gitHostIntegration.js";
7
7
  import { ApiClients } from "../providers/apiClients.js";
8
8
  import { IntegrationService } from "../integrationService.js";
9
- import { IntegrationAuthenticationProvider } from "./integrationAuthenticationProvider.js";
9
+ import { IntegrationAuthenticationProvider, IntegrationAuthenticationSessionDescriptor } from "./integrationAuthenticationProvider.js";
10
10
  import { IntegrationServiceContext } from "../context.js";
11
11
  import { CloudIntegrationService } from "./cloudIntegrationService.js";
12
12
  //#region ../plus/integrations/src/authentication/integrationAuthenticationService.d.ts
@@ -56,6 +56,8 @@ export declare class IntegrationAuthenticationService implements Disposable {
56
56
  /** The package-built per-provider API clients, via the owning service (built once + memoized there). */
57
57
  get apis(): ApiClients;
58
58
  get(providerId: IntegrationIds): Promise<IntegrationAuthenticationProvider>;
59
+ /** The id of the connection a descriptor naming none reads: the primary configured for its host. */
60
+ getPrimaryConnectionId(providerId: IntegrationIds, descriptor: IntegrationAuthenticationSessionDescriptor): string;
59
61
  reset(): Promise<void>;
60
62
  supports(providerId: string): boolean;
61
63
  private ensureProvider;
@@ -49,6 +49,10 @@ var IntegrationAuthenticationService = class {
49
49
  async get(providerId) {
50
50
  return this.ensureProvider(providerId);
51
51
  }
52
+ /** The id of the connection a descriptor naming none reads: the primary configured for its host. */
53
+ getPrimaryConnectionId(providerId, descriptor) {
54
+ return this.configuredIntegrationService.resolveConnectionId(providerId, descriptor);
55
+ }
52
56
  async reset() {
53
57
  await Promise.allSettled(supportedIntegrationIds.map(async (providerId) => (await this.ensureProvider(providerId)).deleteAllSessions()));
54
58
  }
@@ -1 +1 @@
1
- {"version":3,"file":"integrationAuthenticationService.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/integrationAuthenticationService.ts"],"sourcesContent":["import type { GitRemote } from '@gitlens/git/models/remote.js';\nimport { gate } from '@gitlens/utils/decorators/gate.js';\nimport { debug } from '@gitlens/utils/decorators/log.js';\nimport type { Disposable } from '@gitlens/utils/disposable.js';\nimport type { IntegrationIds, SupportedCloudIntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tIssuesCloudHostIntegrationId,\n\tIssuesSelfManagedHostIntegrationId,\n} from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport type { IntegrationService } from '../integrationService.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { ApiClients } from '../providers/apiClients.js';\nimport type { Source } from '../telemetry.js';\nimport { isSelfManagedHostIntegrationId, supportedIntegrationIds } from '../utils/integration.utils.js';\nimport type { CloudIntegrationService } from './cloudIntegrationService.js';\nimport type { ConfiguredIntegrationService } from './configuredIntegrationService.js';\nimport type { IntegrationAuthenticationProvider } from './integrationAuthenticationProvider.js';\nimport { CloudIntegrationAuthenticationProvider } from './integrationAuthenticationProvider.js';\nimport { ObservedAuthenticationProvider } from './observedAuthenticationProvider.js';\n\nexport class IntegrationAuthenticationService implements Disposable {\n\tprivate readonly providers = new Map<IntegrationIds, IntegrationAuthenticationProvider>();\n\n\tconstructor(\n\t\tprivate readonly configuredIntegrationService: ConfiguredIntegrationService,\n\t\treadonly ctx: IntegrationServiceContext,\n\t\t/**\n\t\t * Lazy accessor for the owning {@link IntegrationService}. Injected by the composition root\n\t\t * ({@link createIntegrationManager}) to break the auth↔service construction cycle without a\n\t\t * mutable setter or a host round-trip; only resolved at runtime (long after construction).\n\t\t */\n\t\tprivate readonly getIntegrationService: () => IntegrationService,\n\t\t/** The package's cloud token-exchange client, constructed by the composition root. */\n\t\treadonly cloudIntegrations: CloudIntegrationService,\n\t) {}\n\n\tdispose(): void {\n\t\tthis.providers.forEach(p => p.dispose());\n\t\tthis.providers.clear();\n\t}\n\n\t/**\n\t * Initiates the cloud-integration connect flow on the owning service. Cloud auth providers call\n\t * this when they need a session that doesn't exist yet (see\n\t * {@link CloudIntegrationAuthenticationProvider}).\n\t */\n\tconnectCloudIntegrations(\n\t\tconnect: { integrationIds: SupportedCloudIntegrationIds[]; skipIfConnected?: boolean; skipPreSync?: boolean },\n\t\tsource?: Source,\n\t): Promise<boolean> {\n\t\treturn this.getIntegrationService().connectCloudIntegrations(connect, source);\n\t}\n\n\t/**\n\t * Resolves the connected git-host integration for a `GitRemote`, via the owning service. Lets\n\t * package-internal callers (e.g. the Bitbucket provider) resolve a remote's integration without a\n\t * host round-trip.\n\t */\n\tgetByRemote(remote: GitRemote): Promise<GitHostIntegration | undefined> {\n\t\treturn this.getIntegrationService().getByRemote(remote);\n\t}\n\n\t/** Resolves the per-integration `ignoreSSLErrors` flag via the owning service (host-config-backed). */\n\tignoreSSLErrors(integration: GitHostIntegration | { id: IntegrationIds; domain?: string }): boolean | 'force' {\n\t\treturn this.getIntegrationService().ignoreSSLErrors(integration);\n\t}\n\n\t/** The package-built per-provider API clients, via the owning service (built once + memoized there). */\n\tget apis(): ApiClients {\n\t\treturn this.getIntegrationService().apis;\n\t}\n\n\tasync get(providerId: IntegrationIds): Promise<IntegrationAuthenticationProvider> {\n\t\treturn this.ensureProvider(providerId);\n\t}\n\n\t@debug()\n\tasync reset(): Promise<void> {\n\t\t// TODO: This really isn't ideal, since it will only work for \"cloud\" providers as we won't have any more specific descriptors\n\t\tawait Promise.allSettled(\n\t\t\tsupportedIntegrationIds.map(async providerId =>\n\t\t\t\t(await this.ensureProvider(providerId)).deleteAllSessions(),\n\t\t\t),\n\t\t);\n\t}\n\n\tsupports(providerId: string): boolean {\n\t\t// Linear and Trello are cloud-backed issue integrations (same as Jira): `ensureProvider` registers a\n\t\t// cloud auth provider for them, so a disconnect must offer to sign out of the cloud token too —\n\t\t// otherwise the backend token and configured connection survive the disable.\n\t\tswitch (providerId) {\n\t\t\tcase GitCloudHostIntegrationId.AzureDevOps:\n\t\t\tcase GitSelfManagedHostIntegrationId.AzureDevOpsServer:\n\t\t\tcase GitCloudHostIntegrationId.Bitbucket:\n\t\t\tcase GitCloudHostIntegrationId.GitLab:\n\t\t\tcase IssuesCloudHostIntegrationId.Jira:\n\t\t\tcase IssuesSelfManagedHostIntegrationId.JiraServer:\n\t\t\tcase IssuesCloudHostIntegrationId.Linear:\n\t\t\tcase IssuesCloudHostIntegrationId.Trello:\n\t\t\tcase GitCloudHostIntegrationId.GitHub:\n\t\t\t\treturn true;\n\t\t\tdefault:\n\t\t\t\treturn false;\n\t\t}\n\t}\n\n\t@gate()\n\tprivate async ensureProvider(providerId: IntegrationIds): Promise<IntegrationAuthenticationProvider> {\n\t\tlet provider = this.providers.get(providerId);\n\t\tif (provider != null) return provider;\n\n\t\t// Optional consumer override for custom auth (e.g. a manual-token provider). GitLens omits it —\n\t\t// the package constructs its own cloud providers below (so GitLens's cloud auth needs no host hook).\n\t\tconst hostProvider = await this.ctx.hooks?.createAuthenticationProvider?.({\n\t\t\tid: providerId,\n\t\t});\n\t\tif (hostProvider != null) {\n\t\t\tprovider = new ObservedAuthenticationProvider(\n\t\t\t\thostProvider,\n\t\t\t\tisSelfManagedHostIntegrationId(providerId),\n\t\t\t\t(publication, domains) =>\n\t\t\t\t\tthis.getIntegrationService().onSessionPublishing(providerId, publication, domains),\n\t\t\t);\n\t\t\tthis.providers.set(providerId, provider);\n\t\t\treturn provider;\n\t\t}\n\n\t\t// Every cloud integration shares one provider, parameterized by id (cloud-only).\n\t\tswitch (providerId) {\n\t\t\tcase GitCloudHostIntegrationId.GitHub:\n\t\t\tcase GitSelfManagedHostIntegrationId.CloudGitHubEnterprise:\n\t\t\tcase GitCloudHostIntegrationId.GitLab:\n\t\t\tcase GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted:\n\t\t\tcase GitCloudHostIntegrationId.AzureDevOps:\n\t\t\tcase GitSelfManagedHostIntegrationId.AzureDevOpsServer:\n\t\t\tcase GitCloudHostIntegrationId.Bitbucket:\n\t\t\tcase GitSelfManagedHostIntegrationId.BitbucketServer:\n\t\t\tcase IssuesCloudHostIntegrationId.Jira:\n\t\t\tcase IssuesSelfManagedHostIntegrationId.JiraServer:\n\t\t\tcase IssuesCloudHostIntegrationId.Linear:\n\t\t\tcase IssuesCloudHostIntegrationId.Trello:\n\t\t\t\tprovider = new CloudIntegrationAuthenticationProvider(\n\t\t\t\t\tthis,\n\t\t\t\t\tthis.configuredIntegrationService,\n\t\t\t\t\tproviderId,\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`No authentication provider registered for integration '${String(providerId)}'`);\n\t\t}\n\n\t\tthis.providers.set(providerId, provider);\n\t\treturn provider;\n\t}\n}\n"],"mappings":";;;;;;;;AAuBA,IAAa,mCAAb,MAAoE;CAIjD;CACR;CAMQ;CAER;CAZV,4BAA6B,IAAI,IAAuD;CAExF,YACC,8BACA,KAMA,uBAEA,mBACC;EAVgB,KAAA,+BAAA;EACR,KAAA,MAAA;EAMQ,KAAA,wBAAA;EAER,KAAA,oBAAA;CACP;CAEH,UAAgB;EACf,KAAK,UAAU,SAAQ,MAAK,EAAE,QAAQ,CAAC;EACvC,KAAK,UAAU,MAAM;CACtB;;;;;;CAOA,yBACC,SACA,QACmB;EACnB,OAAO,KAAK,sBAAsB,CAAC,CAAC,yBAAyB,SAAS,MAAM;CAC7E;;;;;;CAOA,YAAY,QAA4D;EACvE,OAAO,KAAK,sBAAsB,CAAC,CAAC,YAAY,MAAM;CACvD;;CAGA,gBAAgB,aAA8F;EAC7G,OAAO,KAAK,sBAAsB,CAAC,CAAC,gBAAgB,WAAW;CAChE;;CAGA,IAAI,OAAmB;EACtB,OAAO,KAAK,sBAAsB,CAAC,CAAC;CACrC;CAEA,MAAM,IAAI,YAAwE;EACjF,OAAO,KAAK,eAAe,UAAU;CACtC;CAEA,MACM,QAAuB;EAE5B,MAAM,QAAQ,WACb,wBAAwB,IAAI,OAAM,gBAChC,MAAM,KAAK,eAAe,UAAU,EAAA,CAAG,kBAAkB,CAC3D,CACD;CACD;CAEA,SAAS,YAA6B;EAIrC,QAAQ,YAAR;GACC,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA,UACC,OAAO;GACR,SACC,OAAO;EACT;CACD;CAEA,MACc,eAAe,YAAwE;EACpG,IAAI,WAAW,KAAK,UAAU,IAAI,UAAU;EAC5C,IAAI,YAAY,MAAM,OAAO;EAI7B,MAAM,eAAe,MAAM,KAAK,IAAI,OAAO,+BAA+B,EACzE,IAAI,WACL,CAAC;EACD,IAAI,gBAAgB,MAAM;GACzB,WAAW,IAAI,+BACd,cACA,+BAA+B,UAAU,IACxC,aAAa,YACb,KAAK,sBAAsB,CAAC,CAAC,oBAAoB,YAAY,aAAa,OAAO,CACnF;GACA,KAAK,UAAU,IAAI,YAAY,QAAQ;GACvC,OAAO;EACR;EAGA,QAAQ,YAAR;GACC,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;IACC,WAAW,IAAI,uCACd,MACA,KAAK,8BACL,UACD;IACA;GACD,SACC,MAAM,IAAI,MAAM,0DAA0D,OAAO,UAAU,EAAE,EAAE;EACjG;EAEA,KAAK,UAAU,IAAI,YAAY,QAAQ;EACvC,OAAO;CACR;AACD;AA9EE,WAAA,CAAA,MAAM,CAAA,GAAA,iCAAA,WAAA,SAAA,IAAA;AA8BN,WAAA,CAAA,KAAK,CAAA,GAAA,iCAAA,WAAA,kBAAA,IAAA"}
1
+ {"version":3,"file":"integrationAuthenticationService.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/integrationAuthenticationService.ts"],"sourcesContent":["import type { GitRemote } from '@gitlens/git/models/remote.js';\nimport { gate } from '@gitlens/utils/decorators/gate.js';\nimport { debug } from '@gitlens/utils/decorators/log.js';\nimport type { Disposable } from '@gitlens/utils/disposable.js';\nimport type { IntegrationIds, SupportedCloudIntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tIssuesCloudHostIntegrationId,\n\tIssuesSelfManagedHostIntegrationId,\n} from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport type { IntegrationService } from '../integrationService.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { ApiClients } from '../providers/apiClients.js';\nimport type { Source } from '../telemetry.js';\nimport { isSelfManagedHostIntegrationId, supportedIntegrationIds } from '../utils/integration.utils.js';\nimport type { CloudIntegrationService } from './cloudIntegrationService.js';\nimport type { ConfiguredIntegrationService } from './configuredIntegrationService.js';\nimport type {\n\tIntegrationAuthenticationProvider,\n\tIntegrationAuthenticationSessionDescriptor,\n} from './integrationAuthenticationProvider.js';\nimport { CloudIntegrationAuthenticationProvider } from './integrationAuthenticationProvider.js';\nimport { ObservedAuthenticationProvider } from './observedAuthenticationProvider.js';\n\nexport class IntegrationAuthenticationService implements Disposable {\n\tprivate readonly providers = new Map<IntegrationIds, IntegrationAuthenticationProvider>();\n\n\tconstructor(\n\t\tprivate readonly configuredIntegrationService: ConfiguredIntegrationService,\n\t\treadonly ctx: IntegrationServiceContext,\n\t\t/**\n\t\t * Lazy accessor for the owning {@link IntegrationService}. Injected by the composition root\n\t\t * ({@link createIntegrationManager}) to break the auth↔service construction cycle without a\n\t\t * mutable setter or a host round-trip; only resolved at runtime (long after construction).\n\t\t */\n\t\tprivate readonly getIntegrationService: () => IntegrationService,\n\t\t/** The package's cloud token-exchange client, constructed by the composition root. */\n\t\treadonly cloudIntegrations: CloudIntegrationService,\n\t) {}\n\n\tdispose(): void {\n\t\tthis.providers.forEach(p => p.dispose());\n\t\tthis.providers.clear();\n\t}\n\n\t/**\n\t * Initiates the cloud-integration connect flow on the owning service. Cloud auth providers call\n\t * this when they need a session that doesn't exist yet (see\n\t * {@link CloudIntegrationAuthenticationProvider}).\n\t */\n\tconnectCloudIntegrations(\n\t\tconnect: { integrationIds: SupportedCloudIntegrationIds[]; skipIfConnected?: boolean; skipPreSync?: boolean },\n\t\tsource?: Source,\n\t): Promise<boolean> {\n\t\treturn this.getIntegrationService().connectCloudIntegrations(connect, source);\n\t}\n\n\t/**\n\t * Resolves the connected git-host integration for a `GitRemote`, via the owning service. Lets\n\t * package-internal callers (e.g. the Bitbucket provider) resolve a remote's integration without a\n\t * host round-trip.\n\t */\n\tgetByRemote(remote: GitRemote): Promise<GitHostIntegration | undefined> {\n\t\treturn this.getIntegrationService().getByRemote(remote);\n\t}\n\n\t/** Resolves the per-integration `ignoreSSLErrors` flag via the owning service (host-config-backed). */\n\tignoreSSLErrors(integration: GitHostIntegration | { id: IntegrationIds; domain?: string }): boolean | 'force' {\n\t\treturn this.getIntegrationService().ignoreSSLErrors(integration);\n\t}\n\n\t/** The package-built per-provider API clients, via the owning service (built once + memoized there). */\n\tget apis(): ApiClients {\n\t\treturn this.getIntegrationService().apis;\n\t}\n\n\tasync get(providerId: IntegrationIds): Promise<IntegrationAuthenticationProvider> {\n\t\treturn this.ensureProvider(providerId);\n\t}\n\n\t/** The id of the connection a descriptor naming none reads: the primary configured for its host. */\n\tgetPrimaryConnectionId(providerId: IntegrationIds, descriptor: IntegrationAuthenticationSessionDescriptor): string {\n\t\treturn this.configuredIntegrationService.resolveConnectionId(providerId, descriptor);\n\t}\n\n\t@debug()\n\tasync reset(): Promise<void> {\n\t\t// TODO: This really isn't ideal, since it will only work for \"cloud\" providers as we won't have any more specific descriptors\n\t\tawait Promise.allSettled(\n\t\t\tsupportedIntegrationIds.map(async providerId =>\n\t\t\t\t(await this.ensureProvider(providerId)).deleteAllSessions(),\n\t\t\t),\n\t\t);\n\t}\n\n\tsupports(providerId: string): boolean {\n\t\t// Linear and Trello are cloud-backed issue integrations (same as Jira): `ensureProvider` registers a\n\t\t// cloud auth provider for them, so a disconnect must offer to sign out of the cloud token too —\n\t\t// otherwise the backend token and configured connection survive the disable.\n\t\tswitch (providerId) {\n\t\t\tcase GitCloudHostIntegrationId.AzureDevOps:\n\t\t\tcase GitSelfManagedHostIntegrationId.AzureDevOpsServer:\n\t\t\tcase GitCloudHostIntegrationId.Bitbucket:\n\t\t\tcase GitCloudHostIntegrationId.GitLab:\n\t\t\tcase IssuesCloudHostIntegrationId.Jira:\n\t\t\tcase IssuesSelfManagedHostIntegrationId.JiraServer:\n\t\t\tcase IssuesCloudHostIntegrationId.Linear:\n\t\t\tcase IssuesCloudHostIntegrationId.Trello:\n\t\t\tcase GitCloudHostIntegrationId.GitHub:\n\t\t\t\treturn true;\n\t\t\tdefault:\n\t\t\t\treturn false;\n\t\t}\n\t}\n\n\t@gate()\n\tprivate async ensureProvider(providerId: IntegrationIds): Promise<IntegrationAuthenticationProvider> {\n\t\tlet provider = this.providers.get(providerId);\n\t\tif (provider != null) return provider;\n\n\t\t// Optional consumer override for custom auth (e.g. a manual-token provider). GitLens omits it —\n\t\t// the package constructs its own cloud providers below (so GitLens's cloud auth needs no host hook).\n\t\tconst hostProvider = await this.ctx.hooks?.createAuthenticationProvider?.({\n\t\t\tid: providerId,\n\t\t});\n\t\tif (hostProvider != null) {\n\t\t\tprovider = new ObservedAuthenticationProvider(\n\t\t\t\thostProvider,\n\t\t\t\tisSelfManagedHostIntegrationId(providerId),\n\t\t\t\t(publication, domains) =>\n\t\t\t\t\tthis.getIntegrationService().onSessionPublishing(providerId, publication, domains),\n\t\t\t);\n\t\t\tthis.providers.set(providerId, provider);\n\t\t\treturn provider;\n\t\t}\n\n\t\t// Every cloud integration shares one provider, parameterized by id (cloud-only).\n\t\tswitch (providerId) {\n\t\t\tcase GitCloudHostIntegrationId.GitHub:\n\t\t\tcase GitSelfManagedHostIntegrationId.CloudGitHubEnterprise:\n\t\t\tcase GitCloudHostIntegrationId.GitLab:\n\t\t\tcase GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted:\n\t\t\tcase GitCloudHostIntegrationId.AzureDevOps:\n\t\t\tcase GitSelfManagedHostIntegrationId.AzureDevOpsServer:\n\t\t\tcase GitCloudHostIntegrationId.Bitbucket:\n\t\t\tcase GitSelfManagedHostIntegrationId.BitbucketServer:\n\t\t\tcase IssuesCloudHostIntegrationId.Jira:\n\t\t\tcase IssuesSelfManagedHostIntegrationId.JiraServer:\n\t\t\tcase IssuesCloudHostIntegrationId.Linear:\n\t\t\tcase IssuesCloudHostIntegrationId.Trello:\n\t\t\t\tprovider = new CloudIntegrationAuthenticationProvider(\n\t\t\t\t\tthis,\n\t\t\t\t\tthis.configuredIntegrationService,\n\t\t\t\t\tproviderId,\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`No authentication provider registered for integration '${String(providerId)}'`);\n\t\t}\n\n\t\tthis.providers.set(providerId, provider);\n\t\treturn provider;\n\t}\n}\n"],"mappings":";;;;;;;;AA0BA,IAAa,mCAAb,MAAoE;CAIjD;CACR;CAMQ;CAER;CAZV,4BAA6B,IAAI,IAAuD;CAExF,YACC,8BACA,KAMA,uBAEA,mBACC;EAVgB,KAAA,+BAAA;EACR,KAAA,MAAA;EAMQ,KAAA,wBAAA;EAER,KAAA,oBAAA;CACP;CAEH,UAAgB;EACf,KAAK,UAAU,SAAQ,MAAK,EAAE,QAAQ,CAAC;EACvC,KAAK,UAAU,MAAM;CACtB;;;;;;CAOA,yBACC,SACA,QACmB;EACnB,OAAO,KAAK,sBAAsB,CAAC,CAAC,yBAAyB,SAAS,MAAM;CAC7E;;;;;;CAOA,YAAY,QAA4D;EACvE,OAAO,KAAK,sBAAsB,CAAC,CAAC,YAAY,MAAM;CACvD;;CAGA,gBAAgB,aAA8F;EAC7G,OAAO,KAAK,sBAAsB,CAAC,CAAC,gBAAgB,WAAW;CAChE;;CAGA,IAAI,OAAmB;EACtB,OAAO,KAAK,sBAAsB,CAAC,CAAC;CACrC;CAEA,MAAM,IAAI,YAAwE;EACjF,OAAO,KAAK,eAAe,UAAU;CACtC;;CAGA,uBAAuB,YAA4B,YAAgE;EAClH,OAAO,KAAK,6BAA6B,oBAAoB,YAAY,UAAU;CACpF;CAEA,MACM,QAAuB;EAE5B,MAAM,QAAQ,WACb,wBAAwB,IAAI,OAAM,gBAChC,MAAM,KAAK,eAAe,UAAU,EAAA,CAAG,kBAAkB,CAC3D,CACD;CACD;CAEA,SAAS,YAA6B;EAIrC,QAAQ,YAAR;GACC,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA,UACC,OAAO;GACR,SACC,OAAO;EACT;CACD;CAEA,MACc,eAAe,YAAwE;EACpG,IAAI,WAAW,KAAK,UAAU,IAAI,UAAU;EAC5C,IAAI,YAAY,MAAM,OAAO;EAI7B,MAAM,eAAe,MAAM,KAAK,IAAI,OAAO,+BAA+B,EACzE,IAAI,WACL,CAAC;EACD,IAAI,gBAAgB,MAAM;GACzB,WAAW,IAAI,+BACd,cACA,+BAA+B,UAAU,IACxC,aAAa,YACb,KAAK,sBAAsB,CAAC,CAAC,oBAAoB,YAAY,aAAa,OAAO,CACnF;GACA,KAAK,UAAU,IAAI,YAAY,QAAQ;GACvC,OAAO;EACR;EAGA,QAAQ,YAAR;GACC,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;GACA,KAAA;IACC,WAAW,IAAI,uCACd,MACA,KAAK,8BACL,UACD;IACA;GACD,SACC,MAAM,IAAI,MAAM,0DAA0D,OAAO,UAAU,EAAE,EAAE;EACjG;EAEA,KAAK,UAAU,IAAI,YAAY,QAAQ;EACvC,OAAO;CACR;AACD;AA9EE,WAAA,CAAA,MAAM,CAAA,GAAA,iCAAA,WAAA,SAAA,IAAA;AA8BN,WAAA,CAAA,KAAK,CAAA,GAAA,iCAAA,WAAA,kBAAA,IAAA"}
@@ -58,6 +58,8 @@ export interface TokenInfo<T extends IntegrationIds | 'gitkraken' = IntegrationI
58
58
  }
59
59
  export interface TokenWithInfo<T extends IntegrationIds = IntegrationIds> extends TokenInfo<T> {
60
60
  readonly accessToken: string;
61
+ /** The token's connection (its session's id), which a failed request's strike counts against. */
62
+ readonly connectionId?: string;
61
63
  }
62
64
  export declare function toTokenInfo<T extends IntegrationIds | 'gitkraken'>(providerId: T, accessToken: string | undefined, info: {
63
65
  cloud: boolean;
@@ -28,7 +28,8 @@ function toTokenWithInfo(providerId, session, altToken) {
28
28
  const accessToken = altToken ?? session.accessToken;
29
29
  return {
30
30
  ...toTokenInfo(providerId, sessionToken, sessionInfo),
31
- accessToken
31
+ accessToken,
32
+ connectionId: session.id
32
33
  };
33
34
  }
34
35
  const CloudIntegrationAuthenticationUriPathPrefix = "did-authenticate-cloud-integration";
@@ -1 +1 @@
1
- {"version":3,"file":"models.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/models.ts"],"sourcesContent":["import { microhash } from '@gitlens/utils/hash.js';\nimport type { IntegrationIds, SupportedCloudIntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tIssuesCloudHostIntegrationId,\n\tIssuesSelfManagedHostIntegrationId,\n\tsupportedOrderedCloudIntegrationIds,\n} from '../constants.js';\n\n/**\n * Mirrors the shape of `vscode.AuthenticationSession` so the integrations\n * package can stay vscode-free. Host adapters bridge VS Code sessions to\n * this shape via {@link AuthenticationProvider}.\n */\nexport interface AuthenticationSessionLike {\n\treadonly id: string;\n\treadonly accessToken: string;\n\treadonly account: { readonly id: string; readonly label: string };\n\treadonly scopes: readonly string[];\n}\n\nexport interface ProviderAuthenticationSession extends AuthenticationSessionLike {\n\treadonly cloud: boolean;\n\treadonly type: CloudIntegrationAuthType | undefined;\n\treadonly expiresAt?: Date;\n\treadonly domain: string;\n\t/**\n\t * The connection's address exactly as it was configured, path and all, when that differs from\n\t * {@link domain}.\n\t *\n\t * `domain` is the IDENTITY a self-managed connection is keyed by, so it is normalized to a bare host\n\t * (`hostFromDomain`) and one host cannot key two connections. An instance mounted below a context path —\n\t * Jira Data Center's `/jira`, a reverse-proxied GitLab or Bitbucket — still has to be ADDRESSED at that\n\t * path, and normalizing it away is what made every request go to the root. Absent when the backend\n\t * reported nothing beyond the host, which is the common case.\n\t */\n\treadonly baseUrl?: string;\n\treadonly protocol?: string;\n\t/** The provider app key paired with the token for providers whose client needs one (e.g. Trello). */\n\treadonly appKey?: string;\n}\n\nexport type SessionPublication = {\n\treadonly session?: ProviderAuthenticationSession;\n\treadonly previousSessionId?: string;\n\treadonly connectionId?: string;\n\t/** A removal has no session from which to recover the affected storage variant. */\n\treadonly cloud?: boolean;\n\treadonly replacePrimary?: boolean;\n};\n\nexport function getSessionIdentity(\n\tsession:\n\t\t| Partial<\n\t\t\t\tPick<\n\t\t\t\t\tProviderAuthenticationSession,\n\t\t\t\t\t'id' | 'accessToken' | 'domain' | 'baseUrl' | 'protocol' | 'appKey' | 'type' | 'cloud'\n\t\t\t\t>\n\t\t >\n\t\t| null\n\t\t| undefined,\n): string | undefined {\n\treturn session == null\n\t\t? undefined\n\t\t: JSON.stringify([\n\t\t\t\tsession.id,\n\t\t\t\tsession.accessToken,\n\t\t\t\tsession.domain,\n\t\t\t\tsession.baseUrl,\n\t\t\t\tsession.protocol,\n\t\t\t\tsession.appKey,\n\t\t\t\tsession.type,\n\t\t\t\tsession.cloud,\n\t\t\t]);\n}\n\nexport interface TokenInfo<T extends IntegrationIds | 'gitkraken' = IntegrationIds | 'gitkraken'> {\n\treadonly providerId: T;\n\t/**\n\t * The first 3 characters of the md5 hash of the token.\n\t * It's a very obfuscated representation of the token that we can use in logs\n\t * to see whether the token survives refreshing or gets updated.\n\t */\n\treadonly microHash: string | undefined;\n\treadonly cloud: boolean;\n\treadonly type: CloudIntegrationAuthType | undefined;\n\treadonly scopes: readonly string[] | undefined;\n\treadonly expiresAt?: Date;\n}\n\nexport interface TokenWithInfo<T extends IntegrationIds = IntegrationIds> extends TokenInfo<T> {\n\treadonly accessToken: string;\n}\n\nexport function toTokenInfo<T extends IntegrationIds | 'gitkraken'>(\n\tproviderId: T,\n\taccessToken: string | undefined,\n\tinfo: { cloud: boolean; type: CloudIntegrationAuthType | undefined; scopes?: readonly string[]; expiresAt?: Date },\n): TokenInfo<T> {\n\treturn {\n\t\tproviderId: providerId,\n\t\tmicroHash: microhash(accessToken, 3),\n\t\tcloud: info.cloud,\n\t\ttype: info.type,\n\t\tscopes: info.scopes,\n\t\texpiresAt: info.expiresAt,\n\t};\n}\n\nexport function toTokenWithInfo<T extends IntegrationIds>(\n\tproviderId: T,\n\tsession: ProviderAuthenticationSession,\n\taltToken?: string,\n): TokenWithInfo<T> {\n\tconst { accessToken: sessionToken, ...sessionInfo } = session;\n\tconst accessToken = altToken ?? session.accessToken;\n\treturn {\n\t\t// pass original token info to form the correlated microhash\n\t\t...toTokenInfo(providerId, sessionToken, sessionInfo),\n\t\t// use the actual token used for the request\n\t\taccessToken: accessToken,\n\t};\n}\n\nexport type TokenOptInfo<T extends IntegrationIds = IntegrationIds> =\n\t| TokenWithInfo<T>\n\t// `connectionId` targets a specific connection (multi-account) when the token still has to be resolved.\n\t// Omitted resolves the provider's primary connection, preserving the pre-multi-account behavior.\n\t| { providerId: T; accessToken?: undefined; connectionId?: string };\n\nexport interface ConfiguredIntegrationDescriptor {\n\t/**\n\t * Stable per-connection identifier, distinct from the domain, so multiple accounts can be\n\t * connected to the same provider+domain simultaneously. For legacy/migrated single connections\n\t * this is the domain (self-managed) or the provider's canonical domain (cloud), which keeps the\n\t * existing secret key (`integration.auth[.cloud]:{id}|{domain}`) valid with zero migration.\n\t */\n\treadonly id: string;\n\t/**\n\t * Whether this is the connection used by default for the provider. Optional: when no descriptor\n\t * for a provider+domain carries an explicit flag, the first one is treated as primary. Only\n\t * {@link ConfiguredIntegrationService.setPrimaryConnection} sets the flag (and clears it on siblings).\n\t */\n\treadonly primary?: boolean;\n\t/** The connection's auth type (`oauth`/`pat`), when known. */\n\treadonly type?: CloudIntegrationAuthType;\n\t/**\n\t * Human-readable account handle for this connection (e.g. the GitHub login). Resolved from the\n\t * provider API (the token backend doesn't expose it); absent until resolved or if the lookup fails.\n\t */\n\treadonly accountName?: string;\n\t/**\n\t * Workspace the connection's token was granted for (Linear), separate from the person. Resolved with the\n\t * connection's own token and kept by connection id; both are set together, or neither is.\n\t */\n\treadonly resourceName?: string;\n\treadonly resourceId?: string;\n\treadonly cloud: boolean;\n\treadonly integrationId: IntegrationIds;\n\treadonly scopes: string;\n\treadonly domain?: string;\n\t/**\n\t * The address this connection was configured with, path included — see\n\t * {@link ProviderAuthenticationSession.baseUrl}.\n\t *\n\t * Mirrored onto the descriptor so reconciliation can tell a re-pointed connection from an unchanged one\n\t * without reading its secret: {@link domain} is normalized to a bare host, so a backend connection moved\n\t * from one context path to another on the SAME host looks identical by domain alone.\n\t */\n\treadonly baseUrl?: string;\n\treadonly expiresAt?: string | Date;\n}\n\nexport interface CloudIntegrationAuthenticationSession {\n\ttype: CloudIntegrationAuthType;\n\taccessToken: string;\n\tdomain: string;\n\texpiresIn: number;\n\tscopes: string;\n\t/** Backend per-connection token identifier, when the provider-tokens backend supports multi-account. */\n\tid?: string;\n\t/** Provider app key paired with the token (e.g. Trello app key), when the backend supplies one. */\n\tappKey?: string;\n}\n\nexport interface CloudIntegrationAuthorization {\n\turl: string;\n}\n\n/** Workspace a connection's token was granted for; only ever a complete pair. */\nexport interface ConnectionResource {\n\treadonly id: string;\n\treadonly name: string;\n}\n\nexport interface CloudIntegrationConnection {\n\ttype: CloudIntegrationAuthType;\n\tprovider: CloudIntegrationType;\n\tdomain: string;\n\t/** Backend per-connection token identifier, when the provider-tokens backend supports multi-account. */\n\tid?: string;\n\t/** Whether the backend marks this connection as the primary/default for the provider. */\n\tprimary?: boolean;\n\t/** Human-readable account handle, when the backend provides it on the connection (may be absent). */\n\taccountName?: string;\n}\n\nexport type CloudIntegrationType =\n\t| 'jira'\n\t| 'jiraServer'\n\t| 'linear'\n\t| 'trello'\n\t| 'gitlab'\n\t| 'github'\n\t| 'bitbucket'\n\t| 'bitbucketServer'\n\t| 'azure'\n\t| 'azureDevopsServer'\n\t| 'githubEnterprise'\n\t| 'gitlabSelfHosted';\n\nexport type CloudIntegrationAuthType = 'oauth' | 'pat';\n\nexport const CloudIntegrationAuthenticationUriPathPrefix = 'did-authenticate-cloud-integration';\n\n/** Returns all supported cloud integration ids. */\nexport function getSupportedCloudIntegrationIds(): readonly SupportedCloudIntegrationIds[] {\n\t// Return the shared const directly (readonly so callers can't mutate it) — this runs on hot,\n\t// per-integration membership paths, so avoid allocating a fresh array per call.\n\treturn supportedOrderedCloudIntegrationIds;\n}\n\n/** Whether `id` is a supported cloud integration id. */\nexport function isSupportedCloudIntegrationId(id: string): id is SupportedCloudIntegrationIds {\n\treturn getSupportedCloudIntegrationIds().includes(id as SupportedCloudIntegrationIds);\n}\n\nexport function isIssueCloudIntegrationId(id: string): id is IssuesCloudHostIntegrationId {\n\tconst issueIds: string[] = Object.values(IssuesCloudHostIntegrationId);\n\treturn issueIds.includes(id);\n}\n\nexport const toIntegrationId: { [key in CloudIntegrationType]: IntegrationIds } = {\n\tjira: IssuesCloudHostIntegrationId.Jira,\n\tjiraServer: IssuesSelfManagedHostIntegrationId.JiraServer,\n\tlinear: IssuesCloudHostIntegrationId.Linear,\n\ttrello: IssuesCloudHostIntegrationId.Trello,\n\tgitlab: GitCloudHostIntegrationId.GitLab,\n\tgithub: GitCloudHostIntegrationId.GitHub,\n\tgithubEnterprise: GitSelfManagedHostIntegrationId.CloudGitHubEnterprise,\n\tgitlabSelfHosted: GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted,\n\tbitbucket: GitCloudHostIntegrationId.Bitbucket,\n\tbitbucketServer: GitSelfManagedHostIntegrationId.BitbucketServer,\n\tazure: GitCloudHostIntegrationId.AzureDevOps,\n\tazureDevopsServer: GitSelfManagedHostIntegrationId.AzureDevOpsServer,\n};\n\nexport const toCloudIntegrationType: { [key in IntegrationIds]: CloudIntegrationType | undefined } = {\n\t[IssuesCloudHostIntegrationId.Jira]: 'jira',\n\t[IssuesSelfManagedHostIntegrationId.JiraServer]: 'jiraServer',\n\t[IssuesCloudHostIntegrationId.Linear]: 'linear',\n\t[IssuesCloudHostIntegrationId.Trello]: 'trello',\n\t[GitCloudHostIntegrationId.GitLab]: 'gitlab',\n\t[GitCloudHostIntegrationId.GitHub]: 'github',\n\t[GitCloudHostIntegrationId.Bitbucket]: 'bitbucket',\n\t[GitCloudHostIntegrationId.AzureDevOps]: 'azure',\n\t[GitSelfManagedHostIntegrationId.AzureDevOpsServer]: 'azureDevopsServer',\n\t[GitSelfManagedHostIntegrationId.CloudGitHubEnterprise]: 'githubEnterprise',\n\t[GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted]: 'gitlabSelfHosted',\n\t[GitSelfManagedHostIntegrationId.BitbucketServer]: 'bitbucketServer',\n};\n"],"mappings":";;;AAoDA,SAAgB,mBACf,SASqB;CACrB,OAAO,WAAW,OACf,KAAA,IACA,KAAK,UAAU;EACf,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;CACT,CAAC;AACJ;AAoBA,SAAgB,YACf,YACA,aACA,MACe;CACf,OAAO;EACM;EACZ,WAAW,UAAU,aAAa,CAAC;EACnC,OAAO,KAAK;EACZ,MAAM,KAAK;EACX,QAAQ,KAAK;EACb,WAAW,KAAK;CACjB;AACD;AAEA,SAAgB,gBACf,YACA,SACA,UACmB;CACnB,MAAM,EAAE,aAAa,cAAc,GAAG,gBAAgB;CACtD,MAAM,cAAc,YAAY,QAAQ;CACxC,OAAO;EAEN,GAAG,YAAY,YAAY,cAAc,WAAW;EAEvC;CACd;AACD;AAqGA,MAAa,8CAA8C;;AAG3D,SAAgB,kCAA2E;CAG1F,OAAO;AACR;;AAGA,SAAgB,8BAA8B,IAAgD;CAC7F,OAAO,gCAAgC,CAAC,CAAC,SAAS,EAAkC;AACrF;AAEA,SAAgB,0BAA0B,IAAgD;CAEzF,OAD2B,OAAO,OAAO,4BAC3B,CAAC,CAAC,SAAS,EAAE;AAC5B;AAEA,MAAa,kBAAqE;CACjF,MAAA;CACA,YAAA;CACA,QAAA;CACA,QAAA;CACA,QAAA;CACA,QAAA;CACA,kBAAA;CACA,kBAAA;CACA,WAAA;CACA,iBAAA;CACA,OAAA;CACA,mBAAA;AACD;AAEA,MAAa,yBAAwF;EAC/D,SAAA;EACY,gBAAA;EACV,WAAA;EACA,WAAA;EACH,WAAA;EACA,WAAA;EACG,cAAA;EACE,gBAAA;EACY,wBAAA;EACI,4BAAA;EACA,6BAAA;EACN,qBAAA;AACpD"}
1
+ {"version":3,"file":"models.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/models.ts"],"sourcesContent":["import { microhash } from '@gitlens/utils/hash.js';\nimport type { IntegrationIds, SupportedCloudIntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tIssuesCloudHostIntegrationId,\n\tIssuesSelfManagedHostIntegrationId,\n\tsupportedOrderedCloudIntegrationIds,\n} from '../constants.js';\n\n/**\n * Mirrors the shape of `vscode.AuthenticationSession` so the integrations\n * package can stay vscode-free. Host adapters bridge VS Code sessions to\n * this shape via {@link AuthenticationProvider}.\n */\nexport interface AuthenticationSessionLike {\n\treadonly id: string;\n\treadonly accessToken: string;\n\treadonly account: { readonly id: string; readonly label: string };\n\treadonly scopes: readonly string[];\n}\n\nexport interface ProviderAuthenticationSession extends AuthenticationSessionLike {\n\treadonly cloud: boolean;\n\treadonly type: CloudIntegrationAuthType | undefined;\n\treadonly expiresAt?: Date;\n\treadonly domain: string;\n\t/**\n\t * The connection's address exactly as it was configured, path and all, when that differs from\n\t * {@link domain}.\n\t *\n\t * `domain` is the IDENTITY a self-managed connection is keyed by, so it is normalized to a bare host\n\t * (`hostFromDomain`) and one host cannot key two connections. An instance mounted below a context path —\n\t * Jira Data Center's `/jira`, a reverse-proxied GitLab or Bitbucket — still has to be ADDRESSED at that\n\t * path, and normalizing it away is what made every request go to the root. Absent when the backend\n\t * reported nothing beyond the host, which is the common case.\n\t */\n\treadonly baseUrl?: string;\n\treadonly protocol?: string;\n\t/** The provider app key paired with the token for providers whose client needs one (e.g. Trello). */\n\treadonly appKey?: string;\n}\n\nexport type SessionPublication = {\n\treadonly session?: ProviderAuthenticationSession;\n\treadonly previousSessionId?: string;\n\treadonly connectionId?: string;\n\t/** A removal has no session from which to recover the affected storage variant. */\n\treadonly cloud?: boolean;\n\treadonly replacePrimary?: boolean;\n};\n\nexport function getSessionIdentity(\n\tsession:\n\t\t| Partial<\n\t\t\t\tPick<\n\t\t\t\t\tProviderAuthenticationSession,\n\t\t\t\t\t'id' | 'accessToken' | 'domain' | 'baseUrl' | 'protocol' | 'appKey' | 'type' | 'cloud'\n\t\t\t\t>\n\t\t >\n\t\t| null\n\t\t| undefined,\n): string | undefined {\n\treturn session == null\n\t\t? undefined\n\t\t: JSON.stringify([\n\t\t\t\tsession.id,\n\t\t\t\tsession.accessToken,\n\t\t\t\tsession.domain,\n\t\t\t\tsession.baseUrl,\n\t\t\t\tsession.protocol,\n\t\t\t\tsession.appKey,\n\t\t\t\tsession.type,\n\t\t\t\tsession.cloud,\n\t\t\t]);\n}\n\nexport interface TokenInfo<T extends IntegrationIds | 'gitkraken' = IntegrationIds | 'gitkraken'> {\n\treadonly providerId: T;\n\t/**\n\t * The first 3 characters of the md5 hash of the token.\n\t * It's a very obfuscated representation of the token that we can use in logs\n\t * to see whether the token survives refreshing or gets updated.\n\t */\n\treadonly microHash: string | undefined;\n\treadonly cloud: boolean;\n\treadonly type: CloudIntegrationAuthType | undefined;\n\treadonly scopes: readonly string[] | undefined;\n\treadonly expiresAt?: Date;\n}\n\nexport interface TokenWithInfo<T extends IntegrationIds = IntegrationIds> extends TokenInfo<T> {\n\treadonly accessToken: string;\n\t/** The token's connection (its session's id), which a failed request's strike counts against. */\n\treadonly connectionId?: string;\n}\n\nexport function toTokenInfo<T extends IntegrationIds | 'gitkraken'>(\n\tproviderId: T,\n\taccessToken: string | undefined,\n\tinfo: { cloud: boolean; type: CloudIntegrationAuthType | undefined; scopes?: readonly string[]; expiresAt?: Date },\n): TokenInfo<T> {\n\treturn {\n\t\tproviderId: providerId,\n\t\tmicroHash: microhash(accessToken, 3),\n\t\tcloud: info.cloud,\n\t\ttype: info.type,\n\t\tscopes: info.scopes,\n\t\texpiresAt: info.expiresAt,\n\t};\n}\n\nexport function toTokenWithInfo<T extends IntegrationIds>(\n\tproviderId: T,\n\tsession: ProviderAuthenticationSession,\n\taltToken?: string,\n): TokenWithInfo<T> {\n\tconst { accessToken: sessionToken, ...sessionInfo } = session;\n\tconst accessToken = altToken ?? session.accessToken;\n\treturn {\n\t\t// pass original token info to form the correlated microhash\n\t\t...toTokenInfo(providerId, sessionToken, sessionInfo),\n\t\t// use the actual token used for the request\n\t\taccessToken: accessToken,\n\t\tconnectionId: session.id,\n\t};\n}\n\nexport type TokenOptInfo<T extends IntegrationIds = IntegrationIds> =\n\t| TokenWithInfo<T>\n\t// `connectionId` targets a specific connection (multi-account) when the token still has to be resolved.\n\t// Omitted resolves the provider's primary connection, preserving the pre-multi-account behavior.\n\t| { providerId: T; accessToken?: undefined; connectionId?: string };\n\nexport interface ConfiguredIntegrationDescriptor {\n\t/**\n\t * Stable per-connection identifier, distinct from the domain, so multiple accounts can be\n\t * connected to the same provider+domain simultaneously. For legacy/migrated single connections\n\t * this is the domain (self-managed) or the provider's canonical domain (cloud), which keeps the\n\t * existing secret key (`integration.auth[.cloud]:{id}|{domain}`) valid with zero migration.\n\t */\n\treadonly id: string;\n\t/**\n\t * Whether this is the connection used by default for the provider. Optional: when no descriptor\n\t * for a provider+domain carries an explicit flag, the first one is treated as primary. Only\n\t * {@link ConfiguredIntegrationService.setPrimaryConnection} sets the flag (and clears it on siblings).\n\t */\n\treadonly primary?: boolean;\n\t/** The connection's auth type (`oauth`/`pat`), when known. */\n\treadonly type?: CloudIntegrationAuthType;\n\t/**\n\t * Human-readable account handle for this connection (e.g. the GitHub login). Resolved from the\n\t * provider API (the token backend doesn't expose it); absent until resolved or if the lookup fails.\n\t */\n\treadonly accountName?: string;\n\t/**\n\t * Workspace the connection's token was granted for (Linear), separate from the person. Resolved with the\n\t * connection's own token and kept by connection id; both are set together, or neither is.\n\t */\n\treadonly resourceName?: string;\n\treadonly resourceId?: string;\n\treadonly cloud: boolean;\n\treadonly integrationId: IntegrationIds;\n\treadonly scopes: string;\n\treadonly domain?: string;\n\t/**\n\t * The address this connection was configured with, path included — see\n\t * {@link ProviderAuthenticationSession.baseUrl}.\n\t *\n\t * Mirrored onto the descriptor so reconciliation can tell a re-pointed connection from an unchanged one\n\t * without reading its secret: {@link domain} is normalized to a bare host, so a backend connection moved\n\t * from one context path to another on the SAME host looks identical by domain alone.\n\t */\n\treadonly baseUrl?: string;\n\treadonly expiresAt?: string | Date;\n}\n\nexport interface CloudIntegrationAuthenticationSession {\n\ttype: CloudIntegrationAuthType;\n\taccessToken: string;\n\tdomain: string;\n\texpiresIn: number;\n\tscopes: string;\n\t/** Backend per-connection token identifier, when the provider-tokens backend supports multi-account. */\n\tid?: string;\n\t/** Provider app key paired with the token (e.g. Trello app key), when the backend supplies one. */\n\tappKey?: string;\n}\n\nexport interface CloudIntegrationAuthorization {\n\turl: string;\n}\n\n/** Workspace a connection's token was granted for; only ever a complete pair. */\nexport interface ConnectionResource {\n\treadonly id: string;\n\treadonly name: string;\n}\n\nexport interface CloudIntegrationConnection {\n\ttype: CloudIntegrationAuthType;\n\tprovider: CloudIntegrationType;\n\tdomain: string;\n\t/** Backend per-connection token identifier, when the provider-tokens backend supports multi-account. */\n\tid?: string;\n\t/** Whether the backend marks this connection as the primary/default for the provider. */\n\tprimary?: boolean;\n\t/** Human-readable account handle, when the backend provides it on the connection (may be absent). */\n\taccountName?: string;\n}\n\nexport type CloudIntegrationType =\n\t| 'jira'\n\t| 'jiraServer'\n\t| 'linear'\n\t| 'trello'\n\t| 'gitlab'\n\t| 'github'\n\t| 'bitbucket'\n\t| 'bitbucketServer'\n\t| 'azure'\n\t| 'azureDevopsServer'\n\t| 'githubEnterprise'\n\t| 'gitlabSelfHosted';\n\nexport type CloudIntegrationAuthType = 'oauth' | 'pat';\n\nexport const CloudIntegrationAuthenticationUriPathPrefix = 'did-authenticate-cloud-integration';\n\n/** Returns all supported cloud integration ids. */\nexport function getSupportedCloudIntegrationIds(): readonly SupportedCloudIntegrationIds[] {\n\t// Return the shared const directly (readonly so callers can't mutate it) — this runs on hot,\n\t// per-integration membership paths, so avoid allocating a fresh array per call.\n\treturn supportedOrderedCloudIntegrationIds;\n}\n\n/** Whether `id` is a supported cloud integration id. */\nexport function isSupportedCloudIntegrationId(id: string): id is SupportedCloudIntegrationIds {\n\treturn getSupportedCloudIntegrationIds().includes(id as SupportedCloudIntegrationIds);\n}\n\nexport function isIssueCloudIntegrationId(id: string): id is IssuesCloudHostIntegrationId {\n\tconst issueIds: string[] = Object.values(IssuesCloudHostIntegrationId);\n\treturn issueIds.includes(id);\n}\n\nexport const toIntegrationId: { [key in CloudIntegrationType]: IntegrationIds } = {\n\tjira: IssuesCloudHostIntegrationId.Jira,\n\tjiraServer: IssuesSelfManagedHostIntegrationId.JiraServer,\n\tlinear: IssuesCloudHostIntegrationId.Linear,\n\ttrello: IssuesCloudHostIntegrationId.Trello,\n\tgitlab: GitCloudHostIntegrationId.GitLab,\n\tgithub: GitCloudHostIntegrationId.GitHub,\n\tgithubEnterprise: GitSelfManagedHostIntegrationId.CloudGitHubEnterprise,\n\tgitlabSelfHosted: GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted,\n\tbitbucket: GitCloudHostIntegrationId.Bitbucket,\n\tbitbucketServer: GitSelfManagedHostIntegrationId.BitbucketServer,\n\tazure: GitCloudHostIntegrationId.AzureDevOps,\n\tazureDevopsServer: GitSelfManagedHostIntegrationId.AzureDevOpsServer,\n};\n\nexport const toCloudIntegrationType: { [key in IntegrationIds]: CloudIntegrationType | undefined } = {\n\t[IssuesCloudHostIntegrationId.Jira]: 'jira',\n\t[IssuesSelfManagedHostIntegrationId.JiraServer]: 'jiraServer',\n\t[IssuesCloudHostIntegrationId.Linear]: 'linear',\n\t[IssuesCloudHostIntegrationId.Trello]: 'trello',\n\t[GitCloudHostIntegrationId.GitLab]: 'gitlab',\n\t[GitCloudHostIntegrationId.GitHub]: 'github',\n\t[GitCloudHostIntegrationId.Bitbucket]: 'bitbucket',\n\t[GitCloudHostIntegrationId.AzureDevOps]: 'azure',\n\t[GitSelfManagedHostIntegrationId.AzureDevOpsServer]: 'azureDevopsServer',\n\t[GitSelfManagedHostIntegrationId.CloudGitHubEnterprise]: 'githubEnterprise',\n\t[GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted]: 'gitlabSelfHosted',\n\t[GitSelfManagedHostIntegrationId.BitbucketServer]: 'bitbucketServer',\n};\n"],"mappings":";;;AAoDA,SAAgB,mBACf,SASqB;CACrB,OAAO,WAAW,OACf,KAAA,IACA,KAAK,UAAU;EACf,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;CACT,CAAC;AACJ;AAsBA,SAAgB,YACf,YACA,aACA,MACe;CACf,OAAO;EACM;EACZ,WAAW,UAAU,aAAa,CAAC;EACnC,OAAO,KAAK;EACZ,MAAM,KAAK;EACX,QAAQ,KAAK;EACb,WAAW,KAAK;CACjB;AACD;AAEA,SAAgB,gBACf,YACA,SACA,UACmB;CACnB,MAAM,EAAE,aAAa,cAAc,GAAG,gBAAgB;CACtD,MAAM,cAAc,YAAY,QAAQ;CACxC,OAAO;EAEN,GAAG,YAAY,YAAY,cAAc,WAAW;EAEvC;EACb,cAAc,QAAQ;CACvB;AACD;AAqGA,MAAa,8CAA8C;;AAG3D,SAAgB,kCAA2E;CAG1F,OAAO;AACR;;AAGA,SAAgB,8BAA8B,IAAgD;CAC7F,OAAO,gCAAgC,CAAC,CAAC,SAAS,EAAkC;AACrF;AAEA,SAAgB,0BAA0B,IAAgD;CAEzF,OAD2B,OAAO,OAAO,4BAC3B,CAAC,CAAC,SAAS,EAAE;AAC5B;AAEA,MAAa,kBAAqE;CACjF,MAAA;CACA,YAAA;CACA,QAAA;CACA,QAAA;CACA,QAAA;CACA,QAAA;CACA,kBAAA;CACA,kBAAA;CACA,WAAA;CACA,iBAAA;CACA,OAAA;CACA,mBAAA;AACD;AAEA,MAAa,yBAAwF;EAC/D,SAAA;EACY,gBAAA;EACV,WAAA;EACA,WAAA;EACH,WAAA;EACA,WAAA;EACG,cAAA;EACE,gBAAA;EACY,wBAAA;EACI,4BAAA;EACA,6BAAA;EACN,qBAAA;AACpD"}
@@ -1,6 +1,6 @@
1
1
  import { CancellationError } from "../../../utils/cancellation.js";
2
- import { getSessionIdentity } from "./models.js";
3
2
  import { areDomainsOnSameHost, hostFromDomain } from "../utils/domain.utils.js";
3
+ import { getSessionIdentity } from "./models.js";
4
4
  //#region ../plus/integrations/src/authentication/observedAuthenticationProvider.ts
5
5
  function domainKey(domain) {
6
6
  return hostFromDomain(domain) ?? domain;
@@ -14,9 +14,9 @@
14
14
  * Deliberately NOT re-armed when the refresh hands back a different token. The GK cloud mints a new access
15
15
  * token on every `/refresh`, so a credential the provider refuses for a reason a refresh cannot fix (retired
16
16
  * scopes being the plain case) would look "rotated" on each attempt: re-arming on that would refresh on every
17
- * read forever and the failure budget — the thing that disconnects and prompts a reconnect — would never
18
- * advance. What resets this instead is {@link clear}, called by every path that replaces or removes the stored
19
- * tokens; a connection that is simply working never reaches here at all.
17
+ * read forever and the connection's failure budget — which, for the primary, disconnects and prompts a
18
+ * reconnect — would never advance. What resets this instead is {@link clear}, called by every path that replaces
19
+ * or removes the stored tokens; a connection that is simply working never reaches here at all.
20
20
  */
21
21
  export declare class RejectedTokenTracker {
22
22
  private readonly _rejections;
@@ -29,7 +29,8 @@ export declare class RejectedTokenTracker {
29
29
  /**
30
30
  * Arms the recovery refresh for `connectionId`. Returns `false` once this connection has had its refresh
31
31
  * (or already has one pending), so the caller falls back to its own failure handling: a token still
32
- * refused after the refresh is not self-healing, and only the failure budget ends in a reconnect prompt.
32
+ * refused after the refresh is not self-healing, and only the failure budget (the primary's) ends in a reconnect
33
+ * prompt.
33
34
  */
34
35
  recordRejection(connectionId: string): boolean;
35
36
  /** Drops all state, for when the stored tokens a rejection could name are replaced or gone. */
@@ -14,9 +14,9 @@
14
14
  * Deliberately NOT re-armed when the refresh hands back a different token. The GK cloud mints a new access
15
15
  * token on every `/refresh`, so a credential the provider refuses for a reason a refresh cannot fix (retired
16
16
  * scopes being the plain case) would look "rotated" on each attempt: re-arming on that would refresh on every
17
- * read forever and the failure budget — the thing that disconnects and prompts a reconnect — would never
18
- * advance. What resets this instead is {@link clear}, called by every path that replaces or removes the stored
19
- * tokens; a connection that is simply working never reaches here at all.
17
+ * read forever and the connection's failure budget — which, for the primary, disconnects and prompts a
18
+ * reconnect — would never advance. What resets this instead is {@link clear}, called by every path that replaces
19
+ * or removes the stored tokens; a connection that is simply working never reaches here at all.
20
20
  */
21
21
  var RejectedTokenTracker = class {
22
22
  _rejections = /* @__PURE__ */ new Map();
@@ -33,7 +33,8 @@ var RejectedTokenTracker = class {
33
33
  /**
34
34
  * Arms the recovery refresh for `connectionId`. Returns `false` once this connection has had its refresh
35
35
  * (or already has one pending), so the caller falls back to its own failure handling: a token still
36
- * refused after the refresh is not self-healing, and only the failure budget ends in a reconnect prompt.
36
+ * refused after the refresh is not self-healing, and only the failure budget (the primary's) ends in a reconnect
37
+ * prompt.
37
38
  */
38
39
  recordRejection(connectionId) {
39
40
  if (this._rejections.has(connectionId)) return false;
@@ -1 +1 @@
1
- {"version":3,"file":"rejectedTokenTracker.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/rejectedTokenTracker.ts"],"sourcesContent":["/**\n * Per-connection recovery state for a cloud token the provider REFUSED — a revoked grant, retired scopes, an\n * app removed from the org. A purely predictive refresh (`expiresIn < 60`) cannot see that case, because the\n * backend still believes the token is valid, so without this the refused token is re-sent on every read and\n * the failure only clears when the user reconnects by hand.\n *\n * One recovery refresh per connection, then the connection is the failure budget's problem:\n * - {@link recordRejection} on an `AuthenticationError` — arms the refresh, or reports that the rejection is\n * not recoverable so the caller counts it as a real failure instead.\n * - {@link claimRefresh} before the next read resolves its session — spends the rejection, so the refresh\n * fires once and no further read of that connection asks the cloud again.\n *\n * Deliberately NOT re-armed when the refresh hands back a different token. The GK cloud mints a new access\n * token on every `/refresh`, so a credential the provider refuses for a reason a refresh cannot fix (retired\n * scopes being the plain case) would look \"rotated\" on each attempt: re-arming on that would refresh on every\n * read forever and the failure budget — the thing that disconnects and prompts a reconnect — would never\n * advance. What resets this instead is {@link clear}, called by every path that replaces or removes the stored\n * tokens; a connection that is simply working never reaches here at all.\n */\nexport class RejectedTokenTracker {\n\tprivate readonly _rejections = new Map<string, 'armed' | 'spent'>();\n\n\t/**\n\t * Whether a read of `connectionId` must force the cloud refresh of a token known to be refused, spending\n\t * the rejection so the refresh runs at most once. Spent here rather than after the read because a resolve\n\t * that throws must not leave the refresh armed and ask the cloud again on the next read.\n\t */\n\tclaimRefresh(connectionId: string): boolean {\n\t\tif (this._rejections.get(connectionId) !== 'armed') return false;\n\n\t\tthis._rejections.set(connectionId, 'spent');\n\t\treturn true;\n\t}\n\n\t/**\n\t * Arms the recovery refresh for `connectionId`. Returns `false` once this connection has had its refresh\n\t * (or already has one pending), so the caller falls back to its own failure handling: a token still\n\t * refused after the refresh is not self-healing, and only the failure budget ends in a reconnect prompt.\n\t */\n\trecordRejection(connectionId: string): boolean {\n\t\tif (this._rejections.has(connectionId)) return false;\n\n\t\tthis._rejections.set(connectionId, 'armed');\n\t\treturn true;\n\t}\n\n\t/** Drops all state, for when the stored tokens a rejection could name are replaced or gone. */\n\tclear(): void {\n\t\tthis._rejections.clear();\n\t}\n\n\t/** Whether any connection is holding a rejection. For assertions; the read path uses {@link claimRefresh}. */\n\tget empty(): boolean {\n\t\treturn this._rejections.size === 0;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,IAAa,uBAAb,MAAkC;CACjC,8BAA+B,IAAI,IAA+B;;;;;;CAOlE,aAAa,cAA+B;EAC3C,IAAI,KAAK,YAAY,IAAI,YAAY,MAAM,SAAS,OAAO;EAE3D,KAAK,YAAY,IAAI,cAAc,OAAO;EAC1C,OAAO;CACR;;;;;;CAOA,gBAAgB,cAA+B;EAC9C,IAAI,KAAK,YAAY,IAAI,YAAY,GAAG,OAAO;EAE/C,KAAK,YAAY,IAAI,cAAc,OAAO;EAC1C,OAAO;CACR;;CAGA,QAAc;EACb,KAAK,YAAY,MAAM;CACxB;;CAGA,IAAI,QAAiB;EACpB,OAAO,KAAK,YAAY,SAAS;CAClC;AACD"}
1
+ {"version":3,"file":"rejectedTokenTracker.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/rejectedTokenTracker.ts"],"sourcesContent":["/**\n * Per-connection recovery state for a cloud token the provider REFUSED — a revoked grant, retired scopes, an\n * app removed from the org. A purely predictive refresh (`expiresIn < 60`) cannot see that case, because the\n * backend still believes the token is valid, so without this the refused token is re-sent on every read and\n * the failure only clears when the user reconnects by hand.\n *\n * One recovery refresh per connection, then the connection is the failure budget's problem:\n * - {@link recordRejection} on an `AuthenticationError` — arms the refresh, or reports that the rejection is\n * not recoverable so the caller counts it as a real failure instead.\n * - {@link claimRefresh} before the next read resolves its session — spends the rejection, so the refresh\n * fires once and no further read of that connection asks the cloud again.\n *\n * Deliberately NOT re-armed when the refresh hands back a different token. The GK cloud mints a new access\n * token on every `/refresh`, so a credential the provider refuses for a reason a refresh cannot fix (retired\n * scopes being the plain case) would look \"rotated\" on each attempt: re-arming on that would refresh on every\n * read forever and the connection's failure budget — which, for the primary, disconnects and prompts a\n * reconnect — would never advance. What resets this instead is {@link clear}, called by every path that replaces\n * or removes the stored tokens; a connection that is simply working never reaches here at all.\n */\nexport class RejectedTokenTracker {\n\tprivate readonly _rejections = new Map<string, 'armed' | 'spent'>();\n\n\t/**\n\t * Whether a read of `connectionId` must force the cloud refresh of a token known to be refused, spending\n\t * the rejection so the refresh runs at most once. Spent here rather than after the read because a resolve\n\t * that throws must not leave the refresh armed and ask the cloud again on the next read.\n\t */\n\tclaimRefresh(connectionId: string): boolean {\n\t\tif (this._rejections.get(connectionId) !== 'armed') return false;\n\n\t\tthis._rejections.set(connectionId, 'spent');\n\t\treturn true;\n\t}\n\n\t/**\n\t * Arms the recovery refresh for `connectionId`. Returns `false` once this connection has had its refresh\n\t * (or already has one pending), so the caller falls back to its own failure handling: a token still\n\t * refused after the refresh is not self-healing, and only the failure budget (the primary's) ends in a reconnect\n\t * prompt.\n\t */\n\trecordRejection(connectionId: string): boolean {\n\t\tif (this._rejections.has(connectionId)) return false;\n\n\t\tthis._rejections.set(connectionId, 'armed');\n\t\treturn true;\n\t}\n\n\t/** Drops all state, for when the stored tokens a rejection could name are replaced or gone. */\n\tclear(): void {\n\t\tthis._rejections.clear();\n\t}\n\n\t/** Whether any connection is holding a rejection. For assertions; the read path uses {@link claimRefresh}. */\n\tget empty(): boolean {\n\t\treturn this._rejections.size === 0;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,IAAa,uBAAb,MAAkC;CACjC,8BAA+B,IAAI,IAA+B;;;;;;CAOlE,aAAa,cAA+B;EAC3C,IAAI,KAAK,YAAY,IAAI,YAAY,MAAM,SAAS,OAAO;EAE3D,KAAK,YAAY,IAAI,cAAc,OAAO;EAC1C,OAAO;CACR;;;;;;;CAQA,gBAAgB,cAA+B;EAC9C,IAAI,KAAK,YAAY,IAAI,YAAY,GAAG,OAAO;EAE/C,KAAK,YAAY,IAAI,cAAc,OAAO;EAC1C,OAAO;CACR;;CAGA,QAAc;EACb,KAAK,YAAY,MAAM;CACxB;;CAGA,IAAI,QAAiB;EACpB,OAAO,KAAK,YAAY,SAAS;CAClC;AACD"}
@@ -8,7 +8,7 @@ import { AuthenticationSessionLike, CloudIntegrationAuthType, CloudIntegrationCo
8
8
  import { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId, IntegrationIds, IssuesCloudHostIntegrationId, IssuesSelfManagedHostIntegrationId, SupportedCloudIntegrationIds, isIntegrationId, isSupportedCloudIntegrationId } from "./constants.js";
9
9
  import { Source, Sources } from "./telemetry.js";
10
10
  import { ConfiguredIntegrationsChangeEvent } from "./authentication/configuredIntegrationService.js";
11
- import { ConnectionStateChangeEvent, ProviderBroadenResult, ProviderLinearProjectShape, ProviderOrganization, ProviderPageInfo, ProviderPagedResult, ProviderRepositoryShape, ProviderResult, ProviderSweepResult, ProviderWarning, ProviderWarningCause, ProviderWarningCauseReason, ProviderWarningKind, ProviderWarningOmission, ProviderWarningOmissionKind, ProviderWarningOmissionRecovery, ProviderWarningOmissionScope, ProviderWarningScope, ProviderWarningSelector, ProviderWarningUnreadableItems, RepositoryIdentity, RepositoryResolution, RepositoryResolutionStatus, ResolveRepositoryResult } from "./results.js";
11
+ import { ConnectionStateChangeEvent, ProviderBroadenResult, ProviderLinearProjectShape, ProviderOrganization, ProviderPageInfo, ProviderPagedResult, ProviderRepositoryShape, ProviderResult, ProviderSweepResult, ProviderSweepTargetRef, ProviderWarning, ProviderWarningCause, ProviderWarningCauseReason, ProviderWarningKind, ProviderWarningOmission, ProviderWarningOmissionKind, ProviderWarningOmissionRecovery, ProviderWarningOmissionScope, ProviderWarningScope, ProviderWarningSelector, ProviderWarningUnreadableItems, RepositoryIdentity, RepositoryResolution, RepositoryResolutionStatus, ResolveRepositoryResult } from "./results.js";
12
12
  import { IssueFilter } from "./providerFilters.js";
13
13
  import { IssueEtagInclude, PullRequestEtagInclude } from "./models/integration.js";
14
14
  import { IssueCountResult, IssueCountScope, PullRequestCountResult, PullRequestCountScope } from "./reads/counts.js";
@@ -80,5 +80,5 @@ export interface IntegrationManagerContext {
80
80
  */
81
81
  export declare function createIntegrationManager(ctx: IntegrationManagerContext): IntegrationManager;
82
82
  //#endregion
83
- export { type Account, type AccountProvider, type AuthenticationSessionLike, type AuthenticationSessionsChangeEvent, type Autolink, type AutolinkReference, type ClosedPullRequestSweepOptions, type CloudIntegrationAuthType, type CloudIntegrationConnection, type CloudIntegrationType, type ConfigChangeEvent, type ConfigProvider, type ConfiguredIntegrationDescriptor, type ConfiguredIntegrationsChangeEvent, type ConnectionStateChangeEvent, type CurrentAccountResult, type DynamicAutolinkReference, type FieldPresence, type GetAutolinkReferencesOptions, GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId, type HttpProvider, type IntegrationAuthenticationProvider, type IntegrationAuthenticationProviderDescriptor, type IntegrationAuthenticationSessionDescriptor, type IntegrationIds, type IntegrationManager, type IntegrationServiceHooks, type IntegrationStorageProvider, type IntegrationsRemoteConfig, type IssueBatchResult, type IssueBatchTarget, type IssueCountResult, type IssueCountScope, type IssueEtagInclude, type IssueFieldGroup, IssueFilter, type IssueProjection, type IssueSearchCapabilities, type IssueSearchCriteria, type IssueSearchRelationship, type IssueStateFilter, IssuesCloudHostIntegrationId, IssuesSelfManagedHostIntegrationId, type ListLinearProjectsOptions, type ListOrgsOptions, type ListProjectsOptions, type ManualTokenAuthProviderOptions, type ProviderAuthenticationSession, type ProviderBroadenOrg, type ProviderBroadenResult, type ProviderLinearProjectShape, type ProviderOrganization, type ProviderPageInfo, type ProviderPagedResult, type ProviderReference, type ProviderRepositoriesInput, type ProviderRepositoryInput, type ProviderRepositoryShape, type ProviderResult, type ProviderSweepResult, type ProviderSweepTarget, type ProviderSweepTargetEvent, type ProviderWarning, type ProviderWarningCause, type ProviderWarningCauseReason, type ProviderWarningKind, type ProviderWarningOmission, type ProviderWarningOmissionKind, type ProviderWarningOmissionRecovery, type ProviderWarningOmissionScope, type ProviderWarningScope, type ProviderWarningSelector, type ProviderWarningUnreadableItems, type PullRequestBatchResult, type PullRequestBatchTarget, type PullRequestBranchResult, type PullRequestBranchTarget, type PullRequestCountResult, type PullRequestCountScope, type PullRequestEtagInclude, type PullRequestFieldGroup, PullRequestFilter, type PullRequestProjection, type PullRequestSearchCapabilities, type PullRequestSearchCriteria, type PullRequestStateFilter, type PullRequestSweepOptions, type RefSet, type RepositoriesProvider, type RepositoryIdentity, type RepositoryResolution, type RepositoryResolutionStatus, type ResolveRepositoryResult, type Source, type Sources, type SupportedCloudIntegrationIds, areDomainsOnSameHost, createManualTokenAuthProvider, getIssueFieldPresence, getPullRequestFieldPresence, hostFromDomain, isIntegrationId, isSupportedCloudIntegrationId, toCloudIntegrationType, toIntegrationId };
83
+ export { type Account, type AccountProvider, type AuthenticationSessionLike, type AuthenticationSessionsChangeEvent, type Autolink, type AutolinkReference, type ClosedPullRequestSweepOptions, type CloudIntegrationAuthType, type CloudIntegrationConnection, type CloudIntegrationType, type ConfigChangeEvent, type ConfigProvider, type ConfiguredIntegrationDescriptor, type ConfiguredIntegrationsChangeEvent, type ConnectionStateChangeEvent, type CurrentAccountResult, type DynamicAutolinkReference, type FieldPresence, type GetAutolinkReferencesOptions, GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId, type HttpProvider, type IntegrationAuthenticationProvider, type IntegrationAuthenticationProviderDescriptor, type IntegrationAuthenticationSessionDescriptor, type IntegrationIds, type IntegrationManager, type IntegrationServiceHooks, type IntegrationStorageProvider, type IntegrationsRemoteConfig, type IssueBatchResult, type IssueBatchTarget, type IssueCountResult, type IssueCountScope, type IssueEtagInclude, type IssueFieldGroup, IssueFilter, type IssueProjection, type IssueSearchCapabilities, type IssueSearchCriteria, type IssueSearchRelationship, type IssueStateFilter, IssuesCloudHostIntegrationId, IssuesSelfManagedHostIntegrationId, type ListLinearProjectsOptions, type ListOrgsOptions, type ListProjectsOptions, type ManualTokenAuthProviderOptions, type ProviderAuthenticationSession, type ProviderBroadenOrg, type ProviderBroadenResult, type ProviderLinearProjectShape, type ProviderOrganization, type ProviderPageInfo, type ProviderPagedResult, type ProviderReference, type ProviderRepositoriesInput, type ProviderRepositoryInput, type ProviderRepositoryShape, type ProviderResult, type ProviderSweepResult, type ProviderSweepTarget, type ProviderSweepTargetEvent, type ProviderSweepTargetRef, type ProviderWarning, type ProviderWarningCause, type ProviderWarningCauseReason, type ProviderWarningKind, type ProviderWarningOmission, type ProviderWarningOmissionKind, type ProviderWarningOmissionRecovery, type ProviderWarningOmissionScope, type ProviderWarningScope, type ProviderWarningSelector, type ProviderWarningUnreadableItems, type PullRequestBatchResult, type PullRequestBatchTarget, type PullRequestBranchResult, type PullRequestBranchTarget, type PullRequestCountResult, type PullRequestCountScope, type PullRequestEtagInclude, type PullRequestFieldGroup, PullRequestFilter, type PullRequestProjection, type PullRequestSearchCapabilities, type PullRequestSearchCriteria, type PullRequestStateFilter, type PullRequestSweepOptions, type RefSet, type RepositoriesProvider, type RepositoryIdentity, type RepositoryResolution, type RepositoryResolutionStatus, type ResolveRepositoryResult, type Source, type Sources, type SupportedCloudIntegrationIds, areDomainsOnSameHost, createManualTokenAuthProvider, getIssueFieldPresence, getPullRequestFieldPresence, hostFromDomain, isIntegrationId, isSupportedCloudIntegrationId, toCloudIntegrationType, toIntegrationId };
84
84
  //# sourceMappingURL=index.d.ts.map