@gitkraken/core-gitlens 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/dist/plus/integrations/authentication/cloudIntegrationService.js +24 -7
  3. package/dist/plus/integrations/authentication/cloudIntegrationService.js.map +1 -1
  4. package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts +40 -27
  5. package/dist/plus/integrations/authentication/configuredIntegrationService.js +92 -22
  6. package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
  7. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts +7 -0
  8. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +22 -8
  9. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
  10. package/dist/plus/integrations/integrationService.js +6 -3
  11. package/dist/plus/integrations/integrationService.js.map +1 -1
  12. package/dist/plus/integrations/models/gitHostIntegration.js +38 -36
  13. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  14. package/dist/plus/integrations/models/integration.d.ts +71 -7
  15. package/dist/plus/integrations/models/integration.js +201 -55
  16. package/dist/plus/integrations/models/integration.js.map +1 -1
  17. package/dist/plus/integrations/models/issuesIntegration.js +9 -9
  18. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  19. package/dist/plus/integrations/providers/azureDevOps.js +1 -1
  20. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  21. package/dist/plus/integrations/providers/azureDevOpsServer.js +1 -1
  22. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
  23. package/dist/plus/integrations/providers/bitbucket-server.js +1 -1
  24. package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
  25. package/dist/plus/integrations/providers/bitbucket.js +1 -1
  26. package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
  27. package/dist/plus/integrations/providers/github.js +3 -6
  28. package/dist/plus/integrations/providers/github.js.map +1 -1
  29. package/dist/plus/integrations/providers/gitlab.js +1 -1
  30. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  31. package/dist/plus/integrations/providers/jira.js +1 -0
  32. package/dist/plus/integrations/providers/jira.js.map +1 -1
  33. package/dist/plus/integrations/providers/linear.js +1 -0
  34. package/dist/plus/integrations/providers/linear.js.map +1 -1
  35. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"configuredIntegrationService.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/configuredIntegrationService.ts"],"sourcesContent":["import { debounce } from '@gitlens/utils/debounce.js';\nimport type { Disposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport { flatten } from '@gitlens/utils/iterable.js';\nimport type {\n\tIntegrationIds,\n\tStoredConfiguredIntegrationDescriptor,\n\tStoredIntegrationConfigurations,\n} from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport { providersMetadata } from '../providers/models.js';\nimport {\n\tareDomainsOnSameHost,\n\tbaseUrlFromDomain,\n\thostFromDomain,\n\tsameConfiguredBaseUrl,\n} from '../utils/domain.utils.js';\nimport { isSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthenticationProvider.js';\nimport type {\n\tCloudIntegrationAuthType,\n\tConfiguredIntegrationDescriptor,\n\tProviderAuthenticationSession,\n} from './models.js';\n\ninterface StoredSession {\n\tid: string;\n\taccessToken: string;\n\taccount?: { label?: string; displayName?: string; id: string };\n\tscopes: string[];\n\tcloud?: boolean;\n\ttype: CloudIntegrationAuthType | undefined;\n\texpiresAt?: string;\n\tdomain?: string;\n\t/** See {@link ProviderAuthenticationSession.baseUrl}: the configured address, path included. */\n\tbaseUrl?: string;\n\tprotocol?: string;\n\t/** The provider app key paired with the token for providers whose client needs one (e.g. Trello). */\n\tappKey?: string;\n}\n\nexport interface ConfiguredIntegrationsChangeEvent {\n\treadonly added: readonly IntegrationIds[];\n\treadonly removed: readonly IntegrationIds[];\n}\n\nexport class ConfiguredIntegrationService implements Disposable {\n\tprivate readonly _onDidChange = new Emitter<ConfiguredIntegrationsChangeEvent>();\n\tprivate storeConfiguredQueue: Promise<void> = Promise.resolve();\n\tget onDidChange(): Event<ConfiguredIntegrationsChangeEvent> {\n\t\treturn this._onDidChange.event;\n\t}\n\n\tconstructor(private readonly ctx: IntegrationServiceContext) {}\n\n\tdispose(): void {\n\t\tthis._onDidChange.dispose();\n\t}\n\n\tprivate _configured?: Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>;\n\tprivate get configured(): Map<IntegrationIds, ConfiguredIntegrationDescriptor[]> {\n\t\tif (this._configured == null) {\n\t\t\tthis._configured = new Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>();\n\n\t\t\tconst storedConfigured = this.ctx.storage.get('integrations:configured');\n\t\t\tfor (const [id, configured] of Object.entries(storedConfigured ?? {}) as [\n\t\t\t\tIntegrationIds,\n\t\t\t\tStoredConfiguredIntegrationDescriptor[],\n\t\t\t][]) {\n\t\t\t\tif (configured == null) continue;\n\n\t\t\t\tconst descriptors = configured.map(d => ({\n\t\t\t\t\t...d,\n\t\t\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? getStoredBaseUrl(d.baseUrl, d.domain) : d.baseUrl,\n\t\t\t\t\tdomain: this.normalizeConfiguredDomain(id, d.domain),\n\t\t\t\t\t// Backfill a stable connection id for pre-multi-account stored data: the domain for\n\t\t\t\t\t// self-managed hosts, or the provider's canonical domain for cloud (which is the legacy\n\t\t\t\t\t// secret-key session id), so existing secrets keep resolving with zero migration. Fall back\n\t\t\t\t\t// to the integration id (never empty) when no domain is known — self-managed providers carry\n\t\t\t\t\t// `domain: ''` in metadata, and an empty id would collide across connections and produce\n\t\t\t\t\t// ambiguous secret keys (`...|`).\n\t\t\t\t\tid: d.id || d.domain || providersMetadata[id]?.domain || id,\n\t\t\t\t\texpiresAt: d.expiresAt ? new Date(d.expiresAt) : undefined,\n\t\t\t\t}));\n\t\t\t\tthis._configured.set(id, descriptors);\n\t\t\t}\n\t\t}\n\n\t\treturn this._configured;\n\t}\n\n\t// Synchronous: reads the in-memory configured map (lazily hydrated from storage). No async work —\n\t// the old async variant only existed to weave in the built-in VS Code GitHub session, which is gone.\n\tgetConfigured(\n\t\tid?: IntegrationIds,\n\t\toptions?: { cloud?: boolean; domain?: string },\n\t): ConfiguredIntegrationDescriptor[] {\n\t\tconst descriptors: ConfiguredIntegrationDescriptor[] = [];\n\n\t\tconst configured =\n\t\t\tid != null\n\t\t\t\t? this.configured.get(id)\n\t\t\t\t: [...flatten<ConfiguredIntegrationDescriptor>(this.configured.values())];\n\t\tif (!configured?.length) return descriptors;\n\n\t\tif (options?.domain != null || options?.cloud != null) {\n\t\t\tfor (const descriptor of configured) {\n\t\t\t\tif (\n\t\t\t\t\t(options?.domain != null &&\n\t\t\t\t\t\t!this.domainsMatch(id ?? descriptor.integrationId, descriptor.domain, options.domain)) ||\n\t\t\t\t\t(options?.cloud === true && !descriptor.cloud) ||\n\t\t\t\t\t(options?.cloud === false && descriptor.cloud)\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tdescriptors.push(descriptor);\n\t\t\t}\n\t\t} else {\n\t\t\tdescriptors.push(...configured);\n\t\t}\n\n\t\treturn descriptors;\n\t}\n\n\tprivate async storeConfigured(): Promise<void> {\n\t\t// We need to convert the map to a record to store\n\t\tconst configured: StoredIntegrationConfigurations = {} as unknown as StoredIntegrationConfigurations;\n\t\tfor (const [id, descriptors] of this.configured) {\n\t\t\tconfigured[id] = descriptors.map(d => ({\n\t\t\t\t...d,\n\t\t\t\texpiresAt: d.expiresAt\n\t\t\t\t\t? d.expiresAt instanceof Date\n\t\t\t\t\t\t? d.expiresAt.toISOString()\n\t\t\t\t\t\t: d.expiresAt\n\t\t\t\t\t: undefined,\n\t\t\t}));\n\t\t}\n\n\t\tconst pending = this.storeConfiguredQueue.then(() =>\n\t\t\tthis.ctx.storage.store('integrations:configured', configured),\n\t\t);\n\t\tthis.storeConfiguredQueue = pending.catch(() => {});\n\t\tawait pending;\n\t}\n\n\tprivate async addOrUpdateConfigured(descriptor: ConfiguredIntegrationDescriptor): Promise<void> {\n\t\tdescriptor = {\n\t\t\t...descriptor,\n\t\t\tbaseUrl: isSelfManagedHostIntegrationId(descriptor.integrationId)\n\t\t\t\t? getStoredBaseUrl(descriptor.baseUrl, descriptor.domain)\n\t\t\t\t: descriptor.baseUrl,\n\t\t\tdomain: this.normalizeConfiguredDomain(descriptor.integrationId, descriptor.domain),\n\t\t};\n\t\tconst descriptors = this.configured.get(descriptor.integrationId) ?? [];\n\t\t// Key connections by their stable id (+ cloud, to preserve legacy local/cloud coexistence) so\n\t\t// multiple accounts on the same provider+domain no longer overwrite each other.\n\t\tconst existing = descriptors.find(d => d.id === descriptor.id && d.cloud === descriptor.cloud);\n\n\t\t// The first connection for a provider/domain is primary by default; an explicit flag (or the\n\t\t// existing value) always wins. Self-managed providers can span multiple hosts under the same\n\t\t// integration id, so each host needs its own primary.\n\t\tconst primaryScope = this.getPrimaryScope(descriptor.integrationId, descriptors, descriptor.domain);\n\t\tconst primary = descriptor.primary ?? existing?.primary ?? !primaryScope.some(d => d.primary);\n\t\t// Preserve a previously-resolved type/accountName when the incoming write doesn't carry them (e.g.\n\t\t// the model re-stores the primary session with an empty account, after reconcile enriched it).\n\t\tconst type = descriptor.type ?? existing?.type;\n\t\tconst accountName = descriptor.accountName ?? existing?.accountName;\n\t\tconst normalized: ConfiguredIntegrationDescriptor = {\n\t\t\t...descriptor,\n\t\t\tprimary: primary,\n\t\t\ttype: type,\n\t\t\taccountName: accountName,\n\t\t};\n\n\t\tlet changed: boolean;\n\t\tif (existing != null) {\n\t\t\t// `baseUrl` is compared alongside `domain`, not folded into it: `domain` is normalized to a bare\n\t\t\t// host, so a connection re-pointed to another context path on the SAME host is identical by domain\n\t\t\t// and would otherwise be treated as unchanged — leaving the stored descriptor, and every warm\n\t\t\t// integration listening for the change, on the old address.\n\t\t\tif (\n\t\t\t\texisting.domain === normalized.domain &&\n\t\t\t\tsameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) &&\n\t\t\t\texisting.expiresAt === normalized.expiresAt &&\n\t\t\t\texisting.scopes === normalized.scopes &&\n\t\t\t\t(existing.primary ?? false) === (normalized.primary ?? false) &&\n\t\t\t\texisting.type === normalized.type &&\n\t\t\t\texisting.accountName === normalized.accountName\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Only fire the change event on address/scopes/primary/type/accountName changes (ignore expiresAt churn)\n\t\t\tchanged =\n\t\t\t\texisting.domain !== normalized.domain ||\n\t\t\t\t!sameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) ||\n\t\t\t\texisting.scopes !== normalized.scopes ||\n\t\t\t\t(existing.primary ?? false) !== (normalized.primary ?? false) ||\n\t\t\t\texisting.type !== normalized.type ||\n\t\t\t\texisting.accountName !== normalized.accountName;\n\n\t\t\t// remove the existing descriptor from the array\n\t\t\tdescriptors.splice(descriptors.indexOf(existing), 1);\n\t\t} else {\n\t\t\tchanged = true;\n\t\t}\n\n\t\tdescriptors.push(normalized);\n\t\tthis.configured.set(descriptor.integrationId, descriptors);\n\n\t\tif (changed) {\n\t\t\tthis.fireChange(descriptor.integrationId);\n\t\t}\n\t\tawait this.storeConfigured();\n\t}\n\n\tprivate async removeConfigured(\n\t\tid: IntegrationIds,\n\t\toptions: { connectionId: string; cloud: boolean | undefined },\n\t): Promise<void> {\n\t\tconst existing = this.configured.get(id);\n\t\tif (existing == null || existing.length === 0) return;\n\n\t\tconst removedPrimaryDomains = new Set<string | undefined>();\n\t\tconst descriptors: ConfiguredIntegrationDescriptor[] = [];\n\t\tfor (const d of existing) {\n\t\t\tif (\n\t\t\t\td.id === options.connectionId &&\n\t\t\t\t(options?.cloud == null ||\n\t\t\t\t\t(options?.cloud === true && d.cloud === true) ||\n\t\t\t\t\t(options?.cloud === false && d.cloud === false))\n\t\t\t) {\n\t\t\t\tif (d.primary) {\n\t\t\t\t\tremovedPrimaryDomains.add(d.domain);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tdescriptors.push(d);\n\t\t}\n\n\t\tif (descriptors.length === existing.length) return; // nothing matched\n\n\t\t// Promote a secondary to primary when the removed connection was the primary and others remain\n\t\t// in the same primary scope, so the provider/host stays in a well-defined connected state.\n\t\tfor (const domain of removedPrimaryDomains) {\n\t\t\tif (descriptors.some(d => this.isInPrimaryScope(id, d, domain) && d.primary)) continue;\n\n\t\t\tconst index = descriptors.findIndex(d => this.isInPrimaryScope(id, d, domain));\n\t\t\tif (index !== -1) {\n\t\t\t\tdescriptors[index] = { ...descriptors[index], primary: true };\n\t\t\t}\n\t\t}\n\n\t\tthis.configured.set(id, descriptors);\n\t\tthis.fireChange(undefined, id);\n\t\tawait this.storeConfigured();\n\t}\n\n\tasync storeSession(id: IntegrationIds, session: ProviderAuthenticationSession): Promise<void> {\n\t\tawait this.writeSecret(id, session);\n\t}\n\n\tasync getStoredSession(\n\t\tid: IntegrationIds,\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tconst sessionId = this.resolveConnectionId(id, descriptor);\n\t\tlet session = descriptor.cloud === true ? undefined : await this.readSecret(id, sessionId, false);\n\n\t\tlet cloudIfMissing = false;\n\t\tif (session != null) {\n\t\t\t// Check the `expiresAt` field\n\t\t\t// If it has an expiresAt property and the key is the old type, then it's a cloud session,\n\t\t\t// so delete it from the local key and\n\t\t\t// store with the \"cloud\" type key, and then use that one.\n\t\t\t// Otherwise it's a local session under the local key, so just return it.\n\t\t\tif (session.expiresAt != null) {\n\t\t\t\tcloudIfMissing = true;\n\t\t\t\tawait Promise.allSettled([this.deleteSecrets(id, session.id), this.writeSecret(id, session)]);\n\t\t\t}\n\t\t}\n\n\t\t// If no local session we try to restore a session with the cloud key\n\t\tif (session == null && descriptor.cloud !== false) {\n\t\t\tcloudIfMissing = true;\n\t\t\tsession = await this.readSecret(id, sessionId, true);\n\t\t}\n\n\t\treturn convertStoredSessionToSession(session, descriptor, cloudIfMissing);\n\t}\n\n\tasync deleteStoredSessions(\n\t\tid: IntegrationIds,\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\tcloud?: boolean,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void> {\n\t\tawait this.deleteSecrets(id, this.resolveConnectionId(id, descriptor), cloud, options);\n\t}\n\n\tasync deleteAllStoredSessions(id: IntegrationIds, cloud?: boolean, domain?: string): Promise<void> {\n\t\tawait this.deleteAllSecrets(id, cloud, domain);\n\t}\n\n\t/**\n\t * Reclaims all stored configuration + secrets for the given (typically retired) integration ids.\n\t * Used to clean up state left behind when an id is removed from the model — e.g. the local\n\t * self-managed `github-enterprise`/`gitlab-self-hosted` ids dropped when integrations went cloud-only.\n\t * Domains are read from the stored config (secrets are keyed by domain); ids with no stored config\n\t * are a no-op.\n\t */\n\tasync purgeStoredConfiguration(ids: readonly string[]): Promise<void> {\n\t\tconst stored =\n\t\t\tthis.ctx.storage.get<Record<string, StoredConfiguredIntegrationDescriptor[] | undefined>>(\n\t\t\t\t'integrations:configured',\n\t\t\t);\n\t\tif (stored == null) return;\n\n\t\tlet changed = false;\n\t\tfor (const id of ids) {\n\t\t\tif (!(id in stored)) continue;\n\n\t\t\tfor (const descriptor of stored[id] ?? []) {\n\t\t\t\t// Mirror the hydration backfill so the secret keys we delete match what was written: fall back\n\t\t\t\t// through domain, the provider's canonical domain, then the integration id (never empty). A\n\t\t\t\t// legacy cloud descriptor stored neither id nor domain, so an empty connection id here would\n\t\t\t\t// miss the canonical-domain secret (e.g. `integration.auth.cloud:github|github.com`) and orphan it.\n\t\t\t\tconst connectionId =\n\t\t\t\t\tdescriptor.id || descriptor.domain || providersMetadata[id as IntegrationIds]?.domain || id;\n\t\t\t\tawait this.ctx.storage.deleteSecret(this.getLocalSecretKey(id as IntegrationIds, connectionId));\n\t\t\t\tawait this.ctx.storage.deleteSecret(this.getCloudSecretKey(id as IntegrationIds, connectionId));\n\t\t\t\tif (descriptor.domain) {\n\t\t\t\t\tawait this.ctx.storage.deleteWorkspace(`connected:${id}:${descriptor.domain}`);\n\t\t\t\t}\n\t\t\t}\n\t\t\tchanged = true;\n\t\t}\n\t\tif (!changed) return;\n\n\t\tconst remaining = Object.fromEntries(Object.entries(stored).filter(([key]) => !ids.includes(key)));\n\t\tawait this.ctx.storage.store('integrations:configured', remaining as StoredIntegrationConfigurations);\n\t}\n\n\tasync deleteSecrets(\n\t\tid: IntegrationIds,\n\t\tconnectionId: string,\n\t\tcloud?: boolean,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void> {\n\t\tif (cloud == null || cloud === false) {\n\t\t\tawait this.ctx.storage.deleteSecret(this.getLocalSecretKey(id, connectionId));\n\t\t}\n\n\t\tif (cloud == null || cloud === true) {\n\t\t\tawait this.ctx.storage.deleteSecret(this.getCloudSecretKey(id, connectionId));\n\t\t}\n\n\t\tif (options?.preserveConfigured) return;\n\n\t\tawait this.removeConfigured(id, { connectionId: connectionId, cloud: cloud });\n\t}\n\n\tasync deleteAllSecrets(id: IntegrationIds, cloud?: boolean, domain?: string): Promise<void> {\n\t\t// Delete every connection's secret (multi-account): secrets are keyed per connection id, so a\n\t\t// single canonical-domain delete would orphan secondary tokens. When a domain is given (self-managed\n\t\t// disconnect of one host), scope to that host so other hosts under the same provider id survive.\n\t\tconst descriptors = this.configured.get(id);\n\t\tconst connectionIds = [\n\t\t\t...new Set(\n\t\t\t\t(domain != null ? descriptors?.filter(c => this.domainsMatch(id, c.domain, domain)) : descriptors)?.map(\n\t\t\t\t\tc => c.id,\n\t\t\t\t) ?? [],\n\t\t\t),\n\t\t];\n\t\tif (connectionIds.length) {\n\t\t\tfor (const connectionId of connectionIds) {\n\t\t\t\tawait this.deleteSecrets(id, connectionId, cloud);\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// A domain-scoped clear that matched nothing means this host has no stored connections, so it must be\n\t\t// a no-op. The canonical-domain fallback below would target a bogus key (self-managed canonical domain\n\t\t// is empty), so skip it.\n\t\tif (domain != null) return;\n\n\t\t// No configured connections (e.g. an orphaned secret with no descriptor): fall back to the\n\t\t// provider's canonical domain, which is the legacy session id for cloud providers. Mirror the\n\t\t// hydration backfill's `|| id` guard so a self-managed provider (empty canonical domain) doesn't\n\t\t// target an ambiguous empty connection id.\n\t\tawait this.deleteSecrets(id, providersMetadata[id]?.domain || id, cloud);\n\t}\n\n\tasync writeSecret(id: IntegrationIds, session: ProviderAuthenticationSession | StoredSession): Promise<void> {\n\t\tawait this.ctx.storage.storeSecret(\n\t\t\tthis.getSecretKey(id, session.id, session.cloud ?? false),\n\t\t\tJSON.stringify(session),\n\t\t);\n\n\t\tawait this.addOrUpdateConfigured({\n\t\t\tid: session.id,\n\t\t\tintegrationId: id,\n\t\t\tdomain: isSelfManagedHostIntegrationId(id) ? session.domain : undefined,\n\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? session.baseUrl : undefined,\n\t\t\texpiresAt: session.expiresAt,\n\t\t\tscopes: session.scopes.join(','),\n\t\t\tcloud: session.cloud ?? false,\n\t\t\ttype: session.type,\n\t\t\taccountName: session.account?.label || undefined,\n\t\t});\n\t}\n\n\tasync readSecret(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t\tcloud: boolean = false,\n\t): Promise<StoredSession | undefined> {\n\t\tlet storedSession: StoredSession | undefined;\n\t\ttry {\n\t\t\tconst sessionJSON = await this.ctx.storage.getSecret(this.getSecretKey(id, sessionId, cloud));\n\t\t\tif (sessionJSON) {\n\t\t\t\tstoredSession = JSON.parse(sessionJSON);\n\t\t\t\tif (storedSession != null) {\n\t\t\t\t\tconst configured = this.configured.get(id);\n\t\t\t\t\tconst connectionId = storedSession.id ?? sessionId;\n\t\t\t\t\tconst sessionCloud = storedSession.cloud ?? cloud;\n\t\t\t\t\tconst domain = isSelfManagedHostIntegrationId(id)\n\t\t\t\t\t\t? (storedSession.domain ?? storedSession.id)\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tif (\n\t\t\t\t\t\tconfigured == null ||\n\t\t\t\t\t\tconfigured.length === 0 ||\n\t\t\t\t\t\t!configured.some(\n\t\t\t\t\t\t\tc => c.id === connectionId && c.integrationId === id && c.cloud === sessionCloud,\n\t\t\t\t\t\t)\n\t\t\t\t\t) {\n\t\t\t\t\t\tawait this.addOrUpdateConfigured({\n\t\t\t\t\t\t\tid: connectionId,\n\t\t\t\t\t\t\tintegrationId: id,\n\t\t\t\t\t\t\tdomain: domain,\n\t\t\t\t\t\t\t// Rebuilt from the secret, so it must carry the address the session does; omitting it\n\t\t\t\t\t\t\t// leaves the descriptor claiming no context path and makes reconcile read every\n\t\t\t\t\t\t\t// routine check-in as a re-pointed connection.\n\t\t\t\t\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? storedSession.baseUrl : undefined,\n\t\t\t\t\t\t\texpiresAt: storedSession.expiresAt,\n\t\t\t\t\t\t\tscopes: storedSession.scopes.join(','),\n\t\t\t\t\t\t\tcloud: sessionCloud,\n\t\t\t\t\t\t\ttype: storedSession.type,\n\t\t\t\t\t\t\taccountName: storedSession.account?.label || undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (_ex) {\n\t\t\ttry {\n\t\t\t\tawait this.deleteSecrets(id, sessionId, cloud);\n\t\t\t} catch {}\n\t\t}\n\t\treturn storedSession;\n\t}\n\n\tprivate getSecretKey(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t\tcloud: boolean = false,\n\t): `integration.auth:${IntegrationIds}|${string}` | `integration.auth.cloud:${IntegrationIds}|${string}` {\n\t\treturn cloud ? this.getCloudSecretKey(id, sessionId) : this.getLocalSecretKey(id, sessionId);\n\t}\n\n\tprivate getLocalSecretKey(id: IntegrationIds, sessionId: string): `integration.auth:${IntegrationIds}|${string}` {\n\t\treturn `integration.auth:${id}|${sessionId}`;\n\t}\n\n\tprivate getCloudSecretKey(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t): `integration.auth.cloud:${IntegrationIds}|${string}` {\n\t\treturn `integration.auth.cloud:${id}|${sessionId}`;\n\t}\n\n\tprivate getPrimaryScope(\n\t\tid: IntegrationIds,\n\t\tdescriptors: ConfiguredIntegrationDescriptor[],\n\t\tdomain: string | undefined,\n\t): ConfiguredIntegrationDescriptor[] {\n\t\treturn isSelfManagedHostIntegrationId(id)\n\t\t\t? descriptors.filter(d => this.isInPrimaryScope(id, d, domain))\n\t\t\t: descriptors;\n\t}\n\n\tprivate isInPrimaryScope(\n\t\tid: IntegrationIds,\n\t\tdescriptor: ConfiguredIntegrationDescriptor,\n\t\tdomain: string | undefined,\n\t): boolean {\n\t\treturn !isSelfManagedHostIntegrationId(id) || this.domainsMatch(id, descriptor.domain, domain);\n\t}\n\n\t/**\n\t * Resolves the connection id used as the secret-key session id for an auth descriptor.\n\t * - If the descriptor targets a specific connection (`connectionId`), that wins.\n\t * - Otherwise returns the primary connection's id for the provider (matching the descriptor's domain\n\t * for self-managed hosts; any connection for cloud hosts, where the stored domain is undefined).\n\t * - Falls back to the descriptor domain when nothing is configured yet, preserving legacy\n\t * (pre-multi-account) reads with zero secret migration.\n\t */\n\tresolveConnectionId(id: IntegrationIds, descriptor: IntegrationAuthenticationSessionDescriptor): string {\n\t\t// An empty connectionId is not a real target — treat it as \"no target\" so it resolves the primary\n\t\t// rather than keying a secret under an empty id.\n\t\tif (descriptor.connectionId) return descriptor.connectionId;\n\n\t\tconst domain = isSelfManagedHostIntegrationId(id) ? descriptor.domain : undefined;\n\t\tconst candidates = this.scopeConnectionCandidates(id, domain, descriptor.cloud);\n\t\treturn (candidates?.find(c => c.primary) ?? candidates?.[0])?.id ?? descriptor.domain;\n\t}\n\n\t/**\n\t * Returns the id of the configured connection an unscoped descriptor resolves to (the primary for the\n\t * descriptor's domain, matching {@link resolveConnectionId}'s selection), or `undefined` when nothing is\n\t * configured for it. Unlike {@link resolveConnectionId} this only returns a genuine per-connection cloud\n\t * token id (never a local/PAT or domain-derived legacy id), so callers can safely use it to scope a cloud\n\t * token fetch to one host of a multi-host self-managed provider, or fall through to the provider-global\n\t * primary endpoint on `undefined`.\n\t */\n\t/**\n\t * The address already stored for a self-managed connection — see {@link ConfiguredIntegrationDescriptor.baseUrl}.\n\t *\n\t * Exists so a session rebuilt from a backend response that omits the domain can keep the address the\n\t * connection was configured with, rather than silently falling back to the bare host and losing a context\n\t * path the user never changed. Looked up by connection id first, then by host for an unscoped fetch.\n\t */\n\t/**\n\t * Whether `connectionId` names a cloud connection configured for `domain`'s host.\n\t *\n\t * The caller-facing question is \"is this id evidence that a token belongs to this host\". A connection id\n\t * can arrive from a caller rather than from our own configuration, so it is only evidence once it matches\n\t * a descriptor we stored for that host — see the host guard in `CloudIntegrationAuthenticationProvider`.\n\t */\n\tisConnectionConfiguredForHost(\n\t\tid: IntegrationIds,\n\t\tconnectionId: string | undefined,\n\t\tdomain: string | undefined,\n\t): boolean {\n\t\tif (connectionId == null) return false;\n\n\t\tconst connection = this.getConfigured(id, { cloud: true }).find(c => c.id === connectionId);\n\t\tif (connection == null) return false;\n\n\t\treturn !isSelfManagedHostIntegrationId(id) || this.domainsMatch(id, connection.domain, domain);\n\t}\n\n\tgetConfiguredBaseUrl(id: IntegrationIds, connectionId: string | undefined, domain?: string): string | undefined {\n\t\tif (!isSelfManagedHostIntegrationId(id)) return undefined;\n\n\t\tconst configured = this.getConfigured(id, { cloud: true });\n\t\tconst byId = connectionId != null ? configured.find(c => c.id === connectionId) : undefined;\n\t\tif (byId != null) return byId.baseUrl;\n\n\t\treturn configured.find(c => this.domainsMatch(id, c.domain, domain))?.baseUrl;\n\t}\n\n\tgetConfiguredConnectionId(id: IntegrationIds, domain: string | undefined, cloud?: boolean): string | undefined {\n\t\tconst scoped = isSelfManagedHostIntegrationId(id) ? domain : undefined;\n\t\tconst candidates = this.scopeConnectionCandidates(id, scoped, cloud);\n\t\tconst connection = candidates?.find(c => c.primary) ?? candidates?.[0];\n\t\tif (connection == null) return undefined;\n\n\t\t// Only return a real per-connection cloud token id. Reject a local (PAT) descriptor (surfaced by\n\t\t// scopeConnectionCandidates' non-cloud fallback) and a domain-derived legacy id (the descriptor's own\n\t\t// domain or the provider's canonical domain — see ConfiguredIntegrationDescriptor.id), either of which\n\t\t// would target `v1/provider-tokens/tokens/{domain}` (rejected as a non-uuid). Callers then fall through\n\t\t// to the provider-scoped primary endpoint.\n\t\tif (!connection.cloud) return undefined;\n\n\t\tconst canonicalDomain = providersMetadata[id]?.domain;\n\t\tif (connection.id === connection.domain || (canonicalDomain && connection.id === canonicalDomain)) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn connection.id;\n\t}\n\n\t/**\n\t * Selects configured descriptors for a provider matching `domain`, further narrowed to the `cloud`\n\t * variant when specified. A connection id can have both a local (PAT) and cloud descriptor whose ids\n\t * differ (e.g. a legacy local descriptor keyed by domain alongside a real cloud token id); scoping by\n\t * `cloud` keeps a cloud-scoped read/delete from resolving the local id (and vice versa). Falls back to\n\t * the domain-only set when no variant matches, so legacy single-variant resolution is unchanged.\n\t */\n\tprivate scopeConnectionCandidates(\n\t\tid: IntegrationIds,\n\t\tdomain: string | undefined,\n\t\tcloud: boolean | undefined,\n\t): ConfiguredIntegrationDescriptor[] | undefined {\n\t\tconst candidates = this.configured.get(id)?.filter(c => this.domainsMatch(id, c.domain, domain));\n\t\tif (cloud == null || candidates == null) return candidates;\n\n\t\tconst scoped = candidates.filter(c => c.cloud === cloud);\n\t\treturn scoped.length ? scoped : candidates;\n\t}\n\n\t/**\n\t * Marks the given connection as the primary/default for the provider and clears the flag on its\n\t * siblings. Persists immediately instead of relying on a session re-store to carry the primary flag.\n\t */\n\tasync setPrimaryConnection(id: IntegrationIds, connectionId: string): Promise<void> {\n\t\tconst descriptors = this.configured.get(id);\n\t\tif (descriptors == null || descriptors.length === 0) return;\n\t\tif (!descriptors.some(d => d.id === connectionId)) return;\n\n\t\tconst target =\n\t\t\tdescriptors.find(d => d.id === connectionId && d.cloud) ?? descriptors.find(d => d.id === connectionId);\n\t\tif (target == null) return;\n\n\t\tconst domain = isSelfManagedHostIntegrationId(id) ? target.domain : undefined;\n\t\t// A connection id can have both a local (PAT) and cloud descriptor. Mark the primary on a single\n\t\t// canonical variant (prefer cloud, since multi-account primaries are cloud-driven) within the\n\t\t// provider/host scope, so other self-managed hosts keep their own default connection.\n\t\tconst primaryIsCloud = descriptors.some(\n\t\t\td => d.id === connectionId && d.cloud && this.isInPrimaryScope(id, d, domain),\n\t\t);\n\n\t\tlet changed = false;\n\t\tconst updated = descriptors.map(d => {\n\t\t\tif (!this.isInPrimaryScope(id, d, domain)) return d;\n\n\t\t\tconst primary = d.id === connectionId && (d.cloud ?? false) === primaryIsCloud;\n\t\t\tif ((d.primary ?? false) === primary) return d;\n\n\t\t\tchanged = true;\n\t\t\treturn { ...d, primary: primary };\n\t\t});\n\t\tif (!changed) return;\n\n\t\tthis.configured.set(id, updated);\n\t\tthis.fireChange(id);\n\t\tawait this.storeConfigured();\n\t}\n\n\t/**\n\t * Removes a single connection (its secret + configured descriptor). If it was the primary and other\n\t * connections remain, a secondary is promoted (see {@link removeConfigured}). Unlike\n\t * {@link deleteAllStoredSessions}, this only affects the targeted connection. Pass `cloud` to scope\n\t * the removal to the cloud/local variant when a local PAT and a cloud session share a connection id.\n\t */\n\tasync deleteConnection(id: IntegrationIds, connectionId: string, cloud?: boolean): Promise<void> {\n\t\tawait this.deleteSecrets(id, connectionId, cloud);\n\t}\n\n\tprivate _addedIds = new Set<IntegrationIds>();\n\tprivate _removedIds = new Set<IntegrationIds>();\n\tprivate _fireChangeDebounced?: () => void;\n\n\tprivate normalizeConfiguredDomain(id: IntegrationIds, domain: string | undefined): string | undefined {\n\t\tif (!isSelfManagedHostIntegrationId(id)) return domain;\n\n\t\treturn hostFromDomain(domain) ?? domain;\n\t}\n\n\tprivate domainsMatch(id: IntegrationIds, first: string | undefined, second: string | undefined): boolean {\n\t\tif (first === second) return true;\n\t\tif (!isSelfManagedHostIntegrationId(id)) return false;\n\t\tif (first == null || second == null) return false;\n\n\t\treturn areDomainsOnSameHost(first, second);\n\t}\n\n\tprivate fireChange(added?: IntegrationIds, removed?: IntegrationIds) {\n\t\tthis._fireChangeDebounced ??= debounce(() => {\n\t\t\tconst added = [...this._addedIds];\n\t\t\tthis._addedIds.clear();\n\t\t\tconst removed = [...this._removedIds];\n\t\t\tthis._removedIds.clear();\n\n\t\t\tthis._onDidChange.fire({ added: added, removed: removed });\n\t\t}, 250);\n\n\t\tif (added != null) {\n\t\t\tthis._addedIds.add(added);\n\t\t}\n\t\tif (removed != null) {\n\t\t\tthis._removedIds.add(removed);\n\t\t}\n\t\tthis._fireChangeDebounced();\n\t}\n}\n\nfunction convertStoredSessionToSession(\n\tstoredSession: StoredSession | undefined,\n\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\tcloudIfMissing: boolean,\n): ProviderAuthenticationSession | undefined {\n\tif (storedSession == null) return undefined;\n\n\treturn {\n\t\tid: storedSession.id,\n\t\taccessToken: storedSession.accessToken,\n\t\taccount: {\n\t\t\tid: storedSession.account?.id ?? '',\n\t\t\tlabel: storedSession.account?.label ?? '',\n\t\t},\n\t\tscopes: storedSession.scopes,\n\t\tcloud: storedSession.cloud ?? cloudIfMissing,\n\t\texpiresAt: storedSession.expiresAt ? new Date(storedSession.expiresAt) : undefined,\n\t\tdomain: storedSession.domain ?? descriptor.domain,\n\t\tbaseUrl: getStoredBaseUrl(storedSession.baseUrl, storedSession.domain, storedSession.protocol),\n\t\tprotocol: storedSession.protocol,\n\t\ttype: storedSession.type,\n\t\t// Carried for providers whose client needs an app key alongside the token (e.g. Trello); without\n\t\t// this a rehydrated session silently loses the key and every read no-ops.\n\t\tappKey: storedSession.appKey,\n\t};\n}\n\nfunction getStoredBaseUrl(\n\tbaseUrl: string | undefined,\n\tdomain: string | undefined,\n\tprotocol?: string,\n): string | undefined {\n\tif (baseUrl != null) return baseUrl;\n\n\tconst legacyUrl = baseUrlFromDomain(domain, protocol);\n\treturn legacyUrl != null && new URL(legacyUrl).pathname !== '/' ? legacyUrl : undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"configuredIntegrationService.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/configuredIntegrationService.ts"],"sourcesContent":["import { debounce } from '@gitlens/utils/debounce.js';\nimport type { Disposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport { flatten } from '@gitlens/utils/iterable.js';\nimport type {\n\tIntegrationIds,\n\tStoredConfiguredIntegrationDescriptor,\n\tStoredIntegrationConfigurations,\n} from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport { providersMetadata } from '../providers/models.js';\nimport {\n\tareDomainsOnSameHost,\n\tbaseUrlFromDomain,\n\thostFromDomain,\n\tsameConfiguredBaseUrl,\n} from '../utils/domain.utils.js';\nimport { isSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthenticationProvider.js';\nimport type {\n\tCloudIntegrationAuthType,\n\tConfiguredIntegrationDescriptor,\n\tProviderAuthenticationSession,\n} from './models.js';\n\ninterface StoredSession {\n\tid: string;\n\taccessToken: string;\n\taccount?: { label?: string; displayName?: string; id: string };\n\tscopes: string[];\n\tcloud?: boolean;\n\ttype: CloudIntegrationAuthType | undefined;\n\texpiresAt?: string;\n\tdomain?: string;\n\t/** See {@link ProviderAuthenticationSession.baseUrl}: the configured address, path included. */\n\tbaseUrl?: string;\n\tprotocol?: string;\n\t/** The provider app key paired with the token for providers whose client needs one (e.g. Trello). */\n\tappKey?: string;\n}\n\nexport interface ConfiguredIntegrationsChangeEvent {\n\treadonly added: readonly IntegrationIds[];\n\treadonly removed: readonly IntegrationIds[];\n}\n\nexport class ConfiguredIntegrationService implements Disposable {\n\tprivate readonly _onDidChange = new Emitter<ConfiguredIntegrationsChangeEvent>();\n\tprivate storeConfiguredQueue: Promise<void> = Promise.resolve();\n\t/**\n\t * One provider's secret and descriptor mutations run one at a time, so a sign-out never interleaves with a\n\t * token being stored: storing writes the secret and then registers its descriptor, and a delete in between would\n\t * miss a connection it cannot see yet, or be undone by the pending write (#5948).\n\t */\n\tprivate readonly _locks = new Map<IntegrationIds, Promise<void>>();\n\t/** Monotonic across providers; see {@link getSignOutMark}. */\n\tprivate _signOutEpoch = 0;\n\t/** The epoch of the latest sign-out per scope: a provider, a self-managed host, or a single connection. */\n\tprivate readonly _signOuts = new Map<string, number>();\n\tget onDidChange(): Event<ConfiguredIntegrationsChangeEvent> {\n\t\treturn this._onDidChange.event;\n\t}\n\n\tconstructor(private readonly ctx: IntegrationServiceContext) {}\n\n\tdispose(): void {\n\t\tthis._onDidChange.dispose();\n\t}\n\n\tprivate _configured?: Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>;\n\tprivate get configured(): Map<IntegrationIds, ConfiguredIntegrationDescriptor[]> {\n\t\tif (this._configured == null) {\n\t\t\tthis._configured = new Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>();\n\n\t\t\tconst storedConfigured = this.ctx.storage.get('integrations:configured');\n\t\t\tfor (const [id, configured] of Object.entries(storedConfigured ?? {}) as [\n\t\t\t\tIntegrationIds,\n\t\t\t\tStoredConfiguredIntegrationDescriptor[],\n\t\t\t][]) {\n\t\t\t\tif (configured == null) continue;\n\n\t\t\t\tconst descriptors = configured.map(d => ({\n\t\t\t\t\t...d,\n\t\t\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? getStoredBaseUrl(d.baseUrl, d.domain) : d.baseUrl,\n\t\t\t\t\tdomain: this.normalizeConfiguredDomain(id, d.domain),\n\t\t\t\t\t// Backfill a stable connection id for pre-multi-account stored data: the domain for\n\t\t\t\t\t// self-managed hosts, or the provider's canonical domain for cloud (which is the legacy\n\t\t\t\t\t// secret-key session id), so existing secrets keep resolving with zero migration. Fall back\n\t\t\t\t\t// to the integration id (never empty) when no domain is known — self-managed providers carry\n\t\t\t\t\t// `domain: ''` in metadata, and an empty id would collide across connections and produce\n\t\t\t\t\t// ambiguous secret keys (`...|`).\n\t\t\t\t\tid: d.id || d.domain || providersMetadata[id]?.domain || id,\n\t\t\t\t\texpiresAt: d.expiresAt ? new Date(d.expiresAt) : undefined,\n\t\t\t\t}));\n\t\t\t\tthis._configured.set(id, descriptors);\n\t\t\t}\n\t\t}\n\n\t\treturn this._configured;\n\t}\n\n\t// Synchronous: reads the in-memory configured map (lazily hydrated from storage). No async work —\n\t// the old async variant only existed to weave in the built-in VS Code GitHub session, which is gone.\n\tgetConfigured(\n\t\tid?: IntegrationIds,\n\t\toptions?: { cloud?: boolean; domain?: string },\n\t): ConfiguredIntegrationDescriptor[] {\n\t\tconst descriptors: ConfiguredIntegrationDescriptor[] = [];\n\n\t\tconst configured =\n\t\t\tid != null\n\t\t\t\t? this.configured.get(id)\n\t\t\t\t: [...flatten<ConfiguredIntegrationDescriptor>(this.configured.values())];\n\t\tif (!configured?.length) return descriptors;\n\n\t\tif (options?.domain != null || options?.cloud != null) {\n\t\t\tfor (const descriptor of configured) {\n\t\t\t\tif (\n\t\t\t\t\t(options?.domain != null &&\n\t\t\t\t\t\t!this.domainsMatch(id ?? descriptor.integrationId, descriptor.domain, options.domain)) ||\n\t\t\t\t\t(options?.cloud === true && !descriptor.cloud) ||\n\t\t\t\t\t(options?.cloud === false && descriptor.cloud)\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tdescriptors.push(descriptor);\n\t\t\t}\n\t\t} else {\n\t\t\tdescriptors.push(...configured);\n\t\t}\n\n\t\treturn descriptors;\n\t}\n\n\tprivate async storeConfigured(): Promise<void> {\n\t\t// We need to convert the map to a record to store\n\t\tconst configured: StoredIntegrationConfigurations = {} as unknown as StoredIntegrationConfigurations;\n\t\tfor (const [id, descriptors] of this.configured) {\n\t\t\tconfigured[id] = descriptors.map(d => ({\n\t\t\t\t...d,\n\t\t\t\texpiresAt: d.expiresAt\n\t\t\t\t\t? d.expiresAt instanceof Date\n\t\t\t\t\t\t? d.expiresAt.toISOString()\n\t\t\t\t\t\t: d.expiresAt\n\t\t\t\t\t: undefined,\n\t\t\t}));\n\t\t}\n\n\t\tconst pending = this.storeConfiguredQueue.then(() =>\n\t\t\tthis.ctx.storage.store('integrations:configured', configured),\n\t\t);\n\t\tthis.storeConfiguredQueue = pending.catch(() => {});\n\t\tawait pending;\n\t}\n\n\tprivate async addOrUpdateConfigured(descriptor: ConfiguredIntegrationDescriptor): Promise<void> {\n\t\tdescriptor = {\n\t\t\t...descriptor,\n\t\t\tbaseUrl: isSelfManagedHostIntegrationId(descriptor.integrationId)\n\t\t\t\t? getStoredBaseUrl(descriptor.baseUrl, descriptor.domain)\n\t\t\t\t: descriptor.baseUrl,\n\t\t\tdomain: this.normalizeConfiguredDomain(descriptor.integrationId, descriptor.domain),\n\t\t};\n\t\tconst descriptors = this.configured.get(descriptor.integrationId) ?? [];\n\t\t// Key connections by their stable id (+ cloud, to preserve legacy local/cloud coexistence) so\n\t\t// multiple accounts on the same provider+domain no longer overwrite each other.\n\t\tconst existing = descriptors.find(d => d.id === descriptor.id && d.cloud === descriptor.cloud);\n\n\t\t// The first connection for a provider/domain is primary by default; an explicit flag (or the\n\t\t// existing value) always wins. Self-managed providers can span multiple hosts under the same\n\t\t// integration id, so each host needs its own primary.\n\t\tconst primaryScope = this.getPrimaryScope(descriptor.integrationId, descriptors, descriptor.domain);\n\t\tconst primary = descriptor.primary ?? existing?.primary ?? !primaryScope.some(d => d.primary);\n\t\t// Preserve a previously-resolved type/accountName when the incoming write doesn't carry them (e.g.\n\t\t// the model re-stores the primary session with an empty account, after reconcile enriched it).\n\t\tconst type = descriptor.type ?? existing?.type;\n\t\tconst accountName = descriptor.accountName ?? existing?.accountName;\n\t\tconst normalized: ConfiguredIntegrationDescriptor = {\n\t\t\t...descriptor,\n\t\t\tprimary: primary,\n\t\t\ttype: type,\n\t\t\taccountName: accountName,\n\t\t};\n\n\t\tlet changed: boolean;\n\t\tif (existing != null) {\n\t\t\t// `baseUrl` is compared alongside `domain`, not folded into it: `domain` is normalized to a bare\n\t\t\t// host, so a connection re-pointed to another context path on the SAME host is identical by domain\n\t\t\t// and would otherwise be treated as unchanged — leaving the stored descriptor, and every warm\n\t\t\t// integration listening for the change, on the old address.\n\t\t\tif (\n\t\t\t\texisting.domain === normalized.domain &&\n\t\t\t\tsameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) &&\n\t\t\t\texisting.expiresAt === normalized.expiresAt &&\n\t\t\t\texisting.scopes === normalized.scopes &&\n\t\t\t\t(existing.primary ?? false) === (normalized.primary ?? false) &&\n\t\t\t\texisting.type === normalized.type &&\n\t\t\t\texisting.accountName === normalized.accountName\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Only fire the change event on address/scopes/primary/type/accountName changes (ignore expiresAt churn)\n\t\t\tchanged =\n\t\t\t\texisting.domain !== normalized.domain ||\n\t\t\t\t!sameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) ||\n\t\t\t\texisting.scopes !== normalized.scopes ||\n\t\t\t\t(existing.primary ?? false) !== (normalized.primary ?? false) ||\n\t\t\t\texisting.type !== normalized.type ||\n\t\t\t\texisting.accountName !== normalized.accountName;\n\n\t\t\t// remove the existing descriptor from the array\n\t\t\tdescriptors.splice(descriptors.indexOf(existing), 1);\n\t\t} else {\n\t\t\tchanged = true;\n\t\t}\n\n\t\tdescriptors.push(normalized);\n\t\tthis.configured.set(descriptor.integrationId, descriptors);\n\n\t\tif (changed) {\n\t\t\tthis.fireChange(descriptor.integrationId);\n\t\t}\n\t\tawait this.storeConfigured();\n\t}\n\n\tprivate async removeConfigured(\n\t\tid: IntegrationIds,\n\t\toptions: { connectionId: string; cloud: boolean | undefined },\n\t): Promise<void> {\n\t\tconst existing = this.configured.get(id);\n\t\tif (existing == null || existing.length === 0) return;\n\n\t\tconst removedPrimaryDomains = new Set<string | undefined>();\n\t\tconst descriptors: ConfiguredIntegrationDescriptor[] = [];\n\t\tfor (const d of existing) {\n\t\t\tif (\n\t\t\t\td.id === options.connectionId &&\n\t\t\t\t(options?.cloud == null ||\n\t\t\t\t\t(options?.cloud === true && d.cloud === true) ||\n\t\t\t\t\t(options?.cloud === false && d.cloud === false))\n\t\t\t) {\n\t\t\t\tif (d.primary) {\n\t\t\t\t\tremovedPrimaryDomains.add(d.domain);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tdescriptors.push(d);\n\t\t}\n\n\t\tif (descriptors.length === existing.length) return; // nothing matched\n\n\t\t// Promote a secondary to primary when the removed connection was the primary and others remain\n\t\t// in the same primary scope, so the provider/host stays in a well-defined connected state.\n\t\tfor (const domain of removedPrimaryDomains) {\n\t\t\tif (descriptors.some(d => this.isInPrimaryScope(id, d, domain) && d.primary)) continue;\n\n\t\t\tconst index = descriptors.findIndex(d => this.isInPrimaryScope(id, d, domain));\n\t\t\tif (index !== -1) {\n\t\t\t\tdescriptors[index] = { ...descriptors[index], primary: true };\n\t\t\t}\n\t\t}\n\n\t\tthis.configured.set(id, descriptors);\n\t\tthis.fireChange(undefined, id);\n\t\tawait this.storeConfigured();\n\t}\n\n\tprivate withLock<T>(id: IntegrationIds, fn: () => Promise<T>): Promise<T> {\n\t\tconst run = (this._locks.get(id) ?? Promise.resolve()).then(fn);\n\t\tconst settled = run.then(\n\t\t\t() => undefined,\n\t\t\t() => undefined,\n\t\t);\n\t\tthis._locks.set(id, settled);\n\t\tvoid settled.then(() => {\n\t\t\tif (this._locks.get(id) === settled) {\n\t\t\t\tthis._locks.delete(id);\n\t\t\t}\n\t\t});\n\t\treturn run;\n\t}\n\n\t/**\n\t * Taken before a token is read or fetched, and handed to {@link storeSession}: a sign-out that lands after it\n\t * (a disconnect, a reauthentication, the connection being removed) refuses the store, so a token fetched across\n\t * it is not written back and reconnected on the next resolution (gitkraken/kepler#3546).\n\t */\n\tgetSignOutMark(): number {\n\t\treturn this._signOutEpoch;\n\t}\n\n\tprivate markSignOut(scope: string): void {\n\t\tthis._signOuts.set(scope, ++this._signOutEpoch);\n\t}\n\n\tprivate getHostScope(id: IntegrationIds, domain: string): string {\n\t\treturn `${id}|host:${hostFromDomain(domain) ?? domain}`;\n\t}\n\n\tprivate isSignedOutSince(id: IntegrationIds, session: ProviderAuthenticationSession, mark: number): boolean {\n\t\tconst since = (scope: string) => (this._signOuts.get(scope) ?? 0) > mark;\n\t\tif (since(id)) return true;\n\t\tif (isSelfManagedHostIntegrationId(id) && session.domain && since(this.getHostScope(id, session.domain))) {\n\t\t\treturn true;\n\t\t}\n\t\treturn since(`${id}|connection:${session.id}`);\n\t}\n\n\t/**\n\t * Stores the session's secret and descriptor together. With a `signOutMark`, nothing is stored, and `false` is\n\t * returned, when a sign-out covering the session landed after the mark was taken.\n\t */\n\tstoreSession(\n\t\tid: IntegrationIds,\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions?: { signOutMark?: number },\n\t): Promise<boolean> {\n\t\treturn this.withLock(id, async () => {\n\t\t\tif (options?.signOutMark != null && this.isSignedOutSince(id, session, options.signOutMark)) return false;\n\n\t\t\tawait this.writeSecret(id, session);\n\t\t\treturn true;\n\t\t});\n\t}\n\n\tgetStoredSession(\n\t\tid: IntegrationIds,\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\t// Under the lock too: reading can repair storage (a legacy local-key cloud session, a missing descriptor).\n\t\treturn this.withLock(id, async () => {\n\t\t\tconst sessionId = this.resolveConnectionId(id, descriptor);\n\t\t\tlet session = descriptor.cloud === true ? undefined : await this.readSecret(id, sessionId, false);\n\n\t\t\tlet cloudIfMissing = false;\n\t\t\tif (session != null) {\n\t\t\t\t// Check the `expiresAt` field\n\t\t\t\t// If it has an expiresAt property and the key is the old type, then it's a cloud session,\n\t\t\t\t// so delete it from the local key and\n\t\t\t\t// store with the \"cloud\" type key, and then use that one.\n\t\t\t\t// Otherwise it's a local session under the local key, so just return it.\n\t\t\t\tif (session.expiresAt != null) {\n\t\t\t\t\tcloudIfMissing = true;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait this.deleteSecrets(id, session.id);\n\t\t\t\t\t\tawait this.writeSecret(id, session);\n\t\t\t\t\t} catch {}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If no local session we try to restore a session with the cloud key\n\t\t\tif (session == null && descriptor.cloud !== false) {\n\t\t\t\tcloudIfMissing = true;\n\t\t\t\tsession = await this.readSecret(id, sessionId, true);\n\t\t\t}\n\n\t\t\treturn convertStoredSessionToSession(session, descriptor, cloudIfMissing);\n\t\t});\n\t}\n\n\t/**\n\t * Deletes one connection's stored session. `signOut` is for a delete that signs the integration out (a forced new\n\t * session): it also refuses the store of any token of the provider, or of the descriptor's self-managed host,\n\t * fetched before it (see {@link getSignOutMark}). A cleanup of a replaced token leaves it unset.\n\t */\n\tdeleteStoredSessions(\n\t\tid: IntegrationIds,\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\tcloud?: boolean,\n\t\toptions?: { preserveConfigured?: boolean; signOut?: boolean },\n\t): Promise<void> {\n\t\treturn this.withLock(id, async () => {\n\t\t\tif (options?.signOut) {\n\t\t\t\tthis.markSignOut(\n\t\t\t\t\tisSelfManagedHostIntegrationId(id) && descriptor.domain\n\t\t\t\t\t\t? this.getHostScope(id, descriptor.domain)\n\t\t\t\t\t\t: id,\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait this.deleteSecrets(id, this.resolveConnectionId(id, descriptor), cloud, options);\n\t\t});\n\t}\n\n\t/** Signs out of every stored session of the provider, or of one self-managed host when `domain` is given. */\n\tdeleteAllStoredSessions(id: IntegrationIds, cloud?: boolean, domain?: string): Promise<void> {\n\t\treturn this.withLock(id, async () => {\n\t\t\tthis.markSignOut(isSelfManagedHostIntegrationId(id) && domain != null ? this.getHostScope(id, domain) : id);\n\t\t\tawait this.deleteAllSecrets(id, cloud, domain);\n\t\t});\n\t}\n\n\t/**\n\t * Reclaims all stored configuration + secrets for the given (typically retired) integration ids.\n\t * Used to clean up state left behind when an id is removed from the model — e.g. the local\n\t * self-managed `github-enterprise`/`gitlab-self-hosted` ids dropped when integrations went cloud-only.\n\t * Domains are read from the stored config (secrets are keyed by domain); ids with no stored config\n\t * are a no-op.\n\t */\n\tasync purgeStoredConfiguration(ids: readonly string[]): Promise<void> {\n\t\tconst stored =\n\t\t\tthis.ctx.storage.get<Record<string, StoredConfiguredIntegrationDescriptor[] | undefined>>(\n\t\t\t\t'integrations:configured',\n\t\t\t);\n\t\tif (stored == null) return;\n\n\t\tlet changed = false;\n\t\tfor (const id of ids) {\n\t\t\tif (!(id in stored)) continue;\n\n\t\t\tfor (const descriptor of stored[id] ?? []) {\n\t\t\t\t// Mirror the hydration backfill so the secret keys we delete match what was written: fall back\n\t\t\t\t// through domain, the provider's canonical domain, then the integration id (never empty). A\n\t\t\t\t// legacy cloud descriptor stored neither id nor domain, so an empty connection id here would\n\t\t\t\t// miss the canonical-domain secret (e.g. `integration.auth.cloud:github|github.com`) and orphan it.\n\t\t\t\tconst connectionId =\n\t\t\t\t\tdescriptor.id || descriptor.domain || providersMetadata[id as IntegrationIds]?.domain || id;\n\t\t\t\tawait this.ctx.storage.deleteSecret(this.getLocalSecretKey(id as IntegrationIds, connectionId));\n\t\t\t\tawait this.ctx.storage.deleteSecret(this.getCloudSecretKey(id as IntegrationIds, connectionId));\n\t\t\t\tif (descriptor.domain) {\n\t\t\t\t\tawait this.ctx.storage.deleteWorkspace(`connected:${id}:${descriptor.domain}`);\n\t\t\t\t}\n\t\t\t}\n\t\t\tchanged = true;\n\t\t}\n\t\tif (!changed) return;\n\n\t\tconst remaining = Object.fromEntries(Object.entries(stored).filter(([key]) => !ids.includes(key)));\n\t\tawait this.ctx.storage.store('integrations:configured', remaining as StoredIntegrationConfigurations);\n\t}\n\n\tprivate async deleteSecrets(\n\t\tid: IntegrationIds,\n\t\tconnectionId: string,\n\t\tcloud?: boolean,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void> {\n\t\tif (cloud == null || cloud === false) {\n\t\t\tawait this.ctx.storage.deleteSecret(this.getLocalSecretKey(id, connectionId));\n\t\t}\n\n\t\tif (cloud == null || cloud === true) {\n\t\t\tawait this.ctx.storage.deleteSecret(this.getCloudSecretKey(id, connectionId));\n\t\t}\n\n\t\tif (options?.preserveConfigured) return;\n\n\t\tawait this.removeConfigured(id, { connectionId: connectionId, cloud: cloud });\n\t}\n\n\tprivate async deleteAllSecrets(id: IntegrationIds, cloud?: boolean, domain?: string): Promise<void> {\n\t\t// Delete every connection's secret (multi-account): secrets are keyed per connection id, so a\n\t\t// single canonical-domain delete would orphan secondary tokens. When a domain is given (self-managed\n\t\t// disconnect of one host), scope to that host so other hosts under the same provider id survive.\n\t\tconst descriptors = this.configured.get(id);\n\t\tconst connectionIds = [\n\t\t\t...new Set(\n\t\t\t\t(domain != null ? descriptors?.filter(c => this.domainsMatch(id, c.domain, domain)) : descriptors)?.map(\n\t\t\t\t\tc => c.id,\n\t\t\t\t) ?? [],\n\t\t\t),\n\t\t];\n\t\tif (connectionIds.length) {\n\t\t\tfor (const connectionId of connectionIds) {\n\t\t\t\tawait this.deleteSecrets(id, connectionId, cloud);\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// A domain-scoped clear that matched nothing means this host has no stored connections, so it must be\n\t\t// a no-op. The canonical-domain fallback below would target a bogus key (self-managed canonical domain\n\t\t// is empty), so skip it.\n\t\tif (domain != null) return;\n\n\t\t// No configured connections (e.g. an orphaned secret with no descriptor): fall back to the\n\t\t// provider's canonical domain, which is the legacy session id for cloud providers. Mirror the\n\t\t// hydration backfill's `|| id` guard so a self-managed provider (empty canonical domain) doesn't\n\t\t// target an ambiguous empty connection id.\n\t\tawait this.deleteSecrets(id, providersMetadata[id]?.domain || id, cloud);\n\t}\n\n\tprivate async writeSecret(\n\t\tid: IntegrationIds,\n\t\tsession: ProviderAuthenticationSession | StoredSession,\n\t): Promise<void> {\n\t\tawait this.ctx.storage.storeSecret(\n\t\t\tthis.getSecretKey(id, session.id, session.cloud ?? false),\n\t\t\tJSON.stringify(session),\n\t\t);\n\n\t\tawait this.addOrUpdateConfigured({\n\t\t\tid: session.id,\n\t\t\tintegrationId: id,\n\t\t\tdomain: isSelfManagedHostIntegrationId(id) ? session.domain : undefined,\n\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? session.baseUrl : undefined,\n\t\t\texpiresAt: session.expiresAt,\n\t\t\tscopes: session.scopes.join(','),\n\t\t\tcloud: session.cloud ?? false,\n\t\t\ttype: session.type,\n\t\t\taccountName: session.account?.label || undefined,\n\t\t});\n\t}\n\n\tprivate async readSecret(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t\tcloud: boolean = false,\n\t): Promise<StoredSession | undefined> {\n\t\tlet storedSession: StoredSession | undefined;\n\t\ttry {\n\t\t\tconst sessionJSON = await this.ctx.storage.getSecret(this.getSecretKey(id, sessionId, cloud));\n\t\t\tif (sessionJSON) {\n\t\t\t\tstoredSession = JSON.parse(sessionJSON);\n\t\t\t\tif (storedSession != null) {\n\t\t\t\t\tconst configured = this.configured.get(id);\n\t\t\t\t\tconst connectionId = storedSession.id ?? sessionId;\n\t\t\t\t\tconst sessionCloud = storedSession.cloud ?? cloud;\n\t\t\t\t\tconst domain = isSelfManagedHostIntegrationId(id)\n\t\t\t\t\t\t? (storedSession.domain ?? storedSession.id)\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tif (\n\t\t\t\t\t\tconfigured == null ||\n\t\t\t\t\t\tconfigured.length === 0 ||\n\t\t\t\t\t\t!configured.some(\n\t\t\t\t\t\t\tc => c.id === connectionId && c.integrationId === id && c.cloud === sessionCloud,\n\t\t\t\t\t\t)\n\t\t\t\t\t) {\n\t\t\t\t\t\tawait this.addOrUpdateConfigured({\n\t\t\t\t\t\t\tid: connectionId,\n\t\t\t\t\t\t\tintegrationId: id,\n\t\t\t\t\t\t\tdomain: domain,\n\t\t\t\t\t\t\t// Rebuilt from the secret, so it must carry the address the session does; omitting it\n\t\t\t\t\t\t\t// leaves the descriptor claiming no context path and makes reconcile read every\n\t\t\t\t\t\t\t// routine check-in as a re-pointed connection.\n\t\t\t\t\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? storedSession.baseUrl : undefined,\n\t\t\t\t\t\t\texpiresAt: storedSession.expiresAt,\n\t\t\t\t\t\t\tscopes: storedSession.scopes.join(','),\n\t\t\t\t\t\t\tcloud: sessionCloud,\n\t\t\t\t\t\t\ttype: storedSession.type,\n\t\t\t\t\t\t\taccountName: storedSession.account?.label || undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (_ex) {\n\t\t\ttry {\n\t\t\t\tawait this.deleteSecrets(id, sessionId, cloud);\n\t\t\t} catch {}\n\t\t}\n\t\treturn storedSession;\n\t}\n\n\tprivate getSecretKey(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t\tcloud: boolean = false,\n\t): `integration.auth:${IntegrationIds}|${string}` | `integration.auth.cloud:${IntegrationIds}|${string}` {\n\t\treturn cloud ? this.getCloudSecretKey(id, sessionId) : this.getLocalSecretKey(id, sessionId);\n\t}\n\n\tprivate getLocalSecretKey(id: IntegrationIds, sessionId: string): `integration.auth:${IntegrationIds}|${string}` {\n\t\treturn `integration.auth:${id}|${sessionId}`;\n\t}\n\n\tprivate getCloudSecretKey(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t): `integration.auth.cloud:${IntegrationIds}|${string}` {\n\t\treturn `integration.auth.cloud:${id}|${sessionId}`;\n\t}\n\n\tprivate getPrimaryScope(\n\t\tid: IntegrationIds,\n\t\tdescriptors: ConfiguredIntegrationDescriptor[],\n\t\tdomain: string | undefined,\n\t): ConfiguredIntegrationDescriptor[] {\n\t\treturn isSelfManagedHostIntegrationId(id)\n\t\t\t? descriptors.filter(d => this.isInPrimaryScope(id, d, domain))\n\t\t\t: descriptors;\n\t}\n\n\tprivate isInPrimaryScope(\n\t\tid: IntegrationIds,\n\t\tdescriptor: ConfiguredIntegrationDescriptor,\n\t\tdomain: string | undefined,\n\t): boolean {\n\t\treturn !isSelfManagedHostIntegrationId(id) || this.domainsMatch(id, descriptor.domain, domain);\n\t}\n\n\t/**\n\t * Resolves the connection id used as the secret-key session id for an auth descriptor.\n\t * - If the descriptor targets a specific connection (`connectionId`), that wins.\n\t * - Otherwise returns the primary connection's id for the provider (matching the descriptor's domain\n\t * for self-managed hosts; any connection for cloud hosts, where the stored domain is undefined).\n\t * - Falls back to the descriptor domain when nothing is configured yet, preserving legacy\n\t * (pre-multi-account) reads with zero secret migration.\n\t */\n\tresolveConnectionId(id: IntegrationIds, descriptor: IntegrationAuthenticationSessionDescriptor): string {\n\t\t// An empty connectionId is not a real target — treat it as \"no target\" so it resolves the primary\n\t\t// rather than keying a secret under an empty id.\n\t\tif (descriptor.connectionId) return descriptor.connectionId;\n\n\t\tconst domain = isSelfManagedHostIntegrationId(id) ? descriptor.domain : undefined;\n\t\tconst candidates = this.scopeConnectionCandidates(id, domain, descriptor.cloud);\n\t\treturn (candidates?.find(c => c.primary) ?? candidates?.[0])?.id ?? descriptor.domain;\n\t}\n\n\t/**\n\t * Returns the id of the configured connection an unscoped descriptor resolves to (the primary for the\n\t * descriptor's domain, matching {@link resolveConnectionId}'s selection), or `undefined` when nothing is\n\t * configured for it. Unlike {@link resolveConnectionId} this only returns a genuine per-connection cloud\n\t * token id (never a local/PAT or domain-derived legacy id), so callers can safely use it to scope a cloud\n\t * token fetch to one host of a multi-host self-managed provider, or fall through to the provider-global\n\t * primary endpoint on `undefined`.\n\t */\n\t/**\n\t * The address already stored for a self-managed connection — see {@link ConfiguredIntegrationDescriptor.baseUrl}.\n\t *\n\t * Exists so a session rebuilt from a backend response that omits the domain can keep the address the\n\t * connection was configured with, rather than silently falling back to the bare host and losing a context\n\t * path the user never changed. Looked up by connection id first, then by host for an unscoped fetch.\n\t */\n\t/**\n\t * Whether `connectionId` names a cloud connection configured for `domain`'s host.\n\t *\n\t * The caller-facing question is \"is this id evidence that a token belongs to this host\". A connection id\n\t * can arrive from a caller rather than from our own configuration, so it is only evidence once it matches\n\t * a descriptor we stored for that host — see the host guard in `CloudIntegrationAuthenticationProvider`.\n\t */\n\tisConnectionConfiguredForHost(\n\t\tid: IntegrationIds,\n\t\tconnectionId: string | undefined,\n\t\tdomain: string | undefined,\n\t): boolean {\n\t\tif (connectionId == null) return false;\n\n\t\tconst connection = this.getConfigured(id, { cloud: true }).find(c => c.id === connectionId);\n\t\tif (connection == null) return false;\n\n\t\treturn !isSelfManagedHostIntegrationId(id) || this.domainsMatch(id, connection.domain, domain);\n\t}\n\n\tgetConfiguredBaseUrl(id: IntegrationIds, connectionId: string | undefined, domain?: string): string | undefined {\n\t\tif (!isSelfManagedHostIntegrationId(id)) return undefined;\n\n\t\tconst configured = this.getConfigured(id, { cloud: true });\n\t\tconst byId = connectionId != null ? configured.find(c => c.id === connectionId) : undefined;\n\t\tif (byId != null) return byId.baseUrl;\n\n\t\treturn configured.find(c => this.domainsMatch(id, c.domain, domain))?.baseUrl;\n\t}\n\n\tgetConfiguredConnectionId(id: IntegrationIds, domain: string | undefined, cloud?: boolean): string | undefined {\n\t\tconst scoped = isSelfManagedHostIntegrationId(id) ? domain : undefined;\n\t\tconst candidates = this.scopeConnectionCandidates(id, scoped, cloud);\n\t\tconst connection = candidates?.find(c => c.primary) ?? candidates?.[0];\n\t\tif (connection == null) return undefined;\n\n\t\t// Only return a real per-connection cloud token id. Reject a local (PAT) descriptor (surfaced by\n\t\t// scopeConnectionCandidates' non-cloud fallback) and a domain-derived legacy id (the descriptor's own\n\t\t// domain or the provider's canonical domain — see ConfiguredIntegrationDescriptor.id), either of which\n\t\t// would target `v1/provider-tokens/tokens/{domain}` (rejected as a non-uuid). Callers then fall through\n\t\t// to the provider-scoped primary endpoint.\n\t\tif (!connection.cloud) return undefined;\n\n\t\tconst canonicalDomain = providersMetadata[id]?.domain;\n\t\tif (connection.id === connection.domain || (canonicalDomain && connection.id === canonicalDomain)) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn connection.id;\n\t}\n\n\t/**\n\t * Selects configured descriptors for a provider matching `domain`, further narrowed to the `cloud`\n\t * variant when specified. A connection id can have both a local (PAT) and cloud descriptor whose ids\n\t * differ (e.g. a legacy local descriptor keyed by domain alongside a real cloud token id); scoping by\n\t * `cloud` keeps a cloud-scoped read/delete from resolving the local id (and vice versa). Falls back to\n\t * the domain-only set when no variant matches, so legacy single-variant resolution is unchanged.\n\t */\n\tprivate scopeConnectionCandidates(\n\t\tid: IntegrationIds,\n\t\tdomain: string | undefined,\n\t\tcloud: boolean | undefined,\n\t): ConfiguredIntegrationDescriptor[] | undefined {\n\t\tconst candidates = this.configured.get(id)?.filter(c => this.domainsMatch(id, c.domain, domain));\n\t\tif (cloud == null || candidates == null) return candidates;\n\n\t\tconst scoped = candidates.filter(c => c.cloud === cloud);\n\t\treturn scoped.length ? scoped : candidates;\n\t}\n\n\t/**\n\t * Marks the given connection as the primary/default for the provider and clears the flag on its\n\t * siblings. Persists immediately instead of relying on a session re-store to carry the primary flag.\n\t */\n\tsetPrimaryConnection(id: IntegrationIds, connectionId: string): Promise<void> {\n\t\treturn this.withLock(id, () => this.setPrimaryConnectionCore(id, connectionId));\n\t}\n\n\tprivate async setPrimaryConnectionCore(id: IntegrationIds, connectionId: string): Promise<void> {\n\t\tconst descriptors = this.configured.get(id);\n\t\tif (descriptors == null || descriptors.length === 0) return;\n\t\tif (!descriptors.some(d => d.id === connectionId)) return;\n\n\t\tconst target =\n\t\t\tdescriptors.find(d => d.id === connectionId && d.cloud) ?? descriptors.find(d => d.id === connectionId);\n\t\tif (target == null) return;\n\n\t\tconst domain = isSelfManagedHostIntegrationId(id) ? target.domain : undefined;\n\t\t// A connection id can have both a local (PAT) and cloud descriptor. Mark the primary on a single\n\t\t// canonical variant (prefer cloud, since multi-account primaries are cloud-driven) within the\n\t\t// provider/host scope, so other self-managed hosts keep their own default connection.\n\t\tconst primaryIsCloud = descriptors.some(\n\t\t\td => d.id === connectionId && d.cloud && this.isInPrimaryScope(id, d, domain),\n\t\t);\n\n\t\tlet changed = false;\n\t\tconst updated = descriptors.map(d => {\n\t\t\tif (!this.isInPrimaryScope(id, d, domain)) return d;\n\n\t\t\tconst primary = d.id === connectionId && (d.cloud ?? false) === primaryIsCloud;\n\t\t\tif ((d.primary ?? false) === primary) return d;\n\n\t\t\tchanged = true;\n\t\t\treturn { ...d, primary: primary };\n\t\t});\n\t\tif (!changed) return;\n\n\t\tthis.configured.set(id, updated);\n\t\tthis.fireChange(id);\n\t\tawait this.storeConfigured();\n\t}\n\n\t/**\n\t * Removes a single connection (its secret + configured descriptor). If it was the primary and other\n\t * connections remain, a secondary is promoted (see {@link removeConfigured}). Unlike\n\t * {@link deleteAllStoredSessions}, this only affects the targeted connection. Pass `cloud` to scope\n\t * the removal to the cloud/local variant when a local PAT and a cloud session share a connection id.\n\t */\n\tdeleteConnection(id: IntegrationIds, connectionId: string, cloud?: boolean): Promise<void> {\n\t\treturn this.withLock(id, async () => {\n\t\t\t// The connection is gone, so a token of it fetched before must not be stored back.\n\t\t\tthis.markSignOut(`${id}|connection:${connectionId}`);\n\t\t\tawait this.deleteSecrets(id, connectionId, cloud);\n\t\t});\n\t}\n\n\tprivate _addedIds = new Set<IntegrationIds>();\n\tprivate _removedIds = new Set<IntegrationIds>();\n\tprivate _fireChangeDebounced?: () => void;\n\n\tprivate normalizeConfiguredDomain(id: IntegrationIds, domain: string | undefined): string | undefined {\n\t\tif (!isSelfManagedHostIntegrationId(id)) return domain;\n\n\t\treturn hostFromDomain(domain) ?? domain;\n\t}\n\n\tprivate domainsMatch(id: IntegrationIds, first: string | undefined, second: string | undefined): boolean {\n\t\tif (first === second) return true;\n\t\tif (!isSelfManagedHostIntegrationId(id)) return false;\n\t\tif (first == null || second == null) return false;\n\n\t\treturn areDomainsOnSameHost(first, second);\n\t}\n\n\tprivate fireChange(added?: IntegrationIds, removed?: IntegrationIds) {\n\t\tthis._fireChangeDebounced ??= debounce(() => {\n\t\t\tconst added = [...this._addedIds];\n\t\t\tthis._addedIds.clear();\n\t\t\tconst removed = [...this._removedIds];\n\t\t\tthis._removedIds.clear();\n\n\t\t\tthis._onDidChange.fire({ added: added, removed: removed });\n\t\t}, 250);\n\n\t\tif (added != null) {\n\t\t\tthis._addedIds.add(added);\n\t\t}\n\t\tif (removed != null) {\n\t\t\tthis._removedIds.add(removed);\n\t\t}\n\t\tthis._fireChangeDebounced();\n\t}\n}\n\nfunction convertStoredSessionToSession(\n\tstoredSession: StoredSession | undefined,\n\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\tcloudIfMissing: boolean,\n): ProviderAuthenticationSession | undefined {\n\tif (storedSession == null) return undefined;\n\n\treturn {\n\t\tid: storedSession.id,\n\t\taccessToken: storedSession.accessToken,\n\t\taccount: {\n\t\t\tid: storedSession.account?.id ?? '',\n\t\t\tlabel: storedSession.account?.label ?? '',\n\t\t},\n\t\tscopes: storedSession.scopes,\n\t\tcloud: storedSession.cloud ?? cloudIfMissing,\n\t\texpiresAt: storedSession.expiresAt ? new Date(storedSession.expiresAt) : undefined,\n\t\tdomain: storedSession.domain ?? descriptor.domain,\n\t\tbaseUrl: getStoredBaseUrl(storedSession.baseUrl, storedSession.domain, storedSession.protocol),\n\t\tprotocol: storedSession.protocol,\n\t\ttype: storedSession.type,\n\t\t// Carried for providers whose client needs an app key alongside the token (e.g. Trello); without\n\t\t// this a rehydrated session silently loses the key and every read no-ops.\n\t\tappKey: storedSession.appKey,\n\t};\n}\n\nfunction getStoredBaseUrl(\n\tbaseUrl: string | undefined,\n\tdomain: string | undefined,\n\tprotocol?: string,\n): string | undefined {\n\tif (baseUrl != null) return baseUrl;\n\n\tconst legacyUrl = baseUrlFromDomain(domain, protocol);\n\treturn legacyUrl != null && new URL(legacyUrl).pathname !== '/' ? legacyUrl : undefined;\n}\n"],"mappings":"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"}
@@ -32,17 +32,24 @@ export interface IntegrationAuthenticationSessionDescriptor {
32
32
  * a previous read), which forces a cloud `/refresh` even when the token's expiry still claims it is valid —
33
33
  * the case a purely time-based refresh cannot see. The refresh token itself never reaches the client: the GK
34
34
  * cloud performs the exchange server-side.
35
+ *
36
+ * `refetch` (sync only) fetches a fresh token instead of serving the stored one, WITHOUT deleting the stored one
37
+ * first: it is replaced only once the fetch returns a session. A forced re-sync used to delete it up front, so a
38
+ * fetch that failed transiently (a GK API 429 or 5xx) left the connection with no token at all, and every read
39
+ * until the next good sync reported it as gone (gitkraken/kepler#3546).
35
40
  */
36
41
  export type GetSessionOptions = {
37
42
  createIfNeeded?: boolean;
38
43
  forceNewSession?: boolean;
39
44
  sync?: never;
45
+ refetch?: never;
40
46
  refreshRejectedToken?: boolean;
41
47
  source?: Sources;
42
48
  } | {
43
49
  createIfNeeded?: never;
44
50
  forceNewSession?: never;
45
51
  sync: boolean;
52
+ refetch?: boolean;
46
53
  refreshRejectedToken?: boolean;
47
54
  source?: Sources;
48
55
  };
@@ -39,16 +39,23 @@ var IntegrationAuthenticationProviderBase = class {
39
39
  async getSession(descriptor, options) {
40
40
  let session;
41
41
  let previousToken;
42
- if (options?.forceNewSession) await this.configuredIntegrationService.deleteStoredSessions(this.authProviderId, {
43
- ...descriptor,
44
- cloud: true
45
- }, true);
46
- else {
47
- session = await this.configuredIntegrationService.getStoredSession(this.authProviderId, options?.sync ? {
42
+ let refetched;
43
+ let signOutMark;
44
+ if (options?.forceNewSession) {
45
+ await this.configuredIntegrationService.deleteStoredSessions(this.authProviderId, {
46
+ ...descriptor,
47
+ cloud: true
48
+ }, true, { signOut: true });
49
+ signOutMark = this.configuredIntegrationService.getSignOutMark();
50
+ } else {
51
+ signOutMark = this.configuredIntegrationService.getSignOutMark();
52
+ const stored = await this.configuredIntegrationService.getStoredSession(this.authProviderId, options?.sync ? {
48
53
  ...descriptor,
49
54
  cloud: true
50
55
  } : descriptor);
51
- previousToken = session?.accessToken;
56
+ previousToken = stored?.accessToken;
57
+ if (options?.sync && options.refetch) refetched = stored;
58
+ else session = stored;
52
59
  }
53
60
  const isExpiredSession = session?.expiresAt != null && new Date(session.expiresAt).getTime() < Date.now();
54
61
  const refreshRejectedToken = options?.refreshRejectedToken === true && session != null;
@@ -58,7 +65,14 @@ var IntegrationAuthenticationProviderBase = class {
58
65
  refreshIfExpired: isExpiredSession,
59
66
  refreshRejectedToken
60
67
  });
61
- if (session != null) await this.configuredIntegrationService.storeSession(this.authProviderId, session);
68
+ if (session != null && !await this.configuredIntegrationService.storeSession(this.authProviderId, session, { signOutMark })) session = void 0;
69
+ if (session != null) {
70
+ if (refetched?.id != null && refetched.id !== session.id) await this.configuredIntegrationService.deleteStoredSessions(this.authProviderId, {
71
+ ...descriptor,
72
+ connectionId: refetched.id,
73
+ cloud: true
74
+ }, true, { preserveConfigured: true });
75
+ }
62
76
  }
63
77
  if (previousToken !== session?.accessToken) this.fireChange();
64
78
  return session;
@@ -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 */\nexport type GetSessionOptions =\n\t| {\n\t\t\tcreateIfNeeded?: boolean;\n\t\t\tforceNewSession?: boolean;\n\t\t\tsync?: 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\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\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.\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);\n\t\t} else {\n\t\t\tsession = 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 = session?.accessToken;\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\n\t\t\tif (session != null) {\n\t\t\t\tawait this.configuredIntegrationService.storeSession(this.authProviderId, session);\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":";;;;;;;AAaA,MAAM,oBAAoB,KAAK,KAAK;AA8EpC,IAAe,wCAAf,MAE+C;CAI1B;CACA;CAJpB,cAA+C,CAAC;CAEhD,YACC,uBACA,8BACC;EAFkB,KAAA,wBAAA;EACA,KAAA,+BAAA;CACjB;CAEH,UAAgB;EACf,KAAK,YAAY,SAAQ,MAAK,EAAE,QAAQ,CAAC;CAC1C;CAEA,eAAgC,IAAI,QAAc;CAClD,IAAI,cAA2B;EAC9B,OAAO,KAAK,aAAa;CAC1B;CAIA,MACM,cACL,YACA,SACgB;EAChB,MAAM,SAAS,+BAA+B,KAAK,cAAc,IAAI,YAAY,SAAS,KAAA;EAC1F,MAAM,aAAa,KAAK,6BAA6B,cAAc,KAAK,gBAAgB,EAC/E,OACT,CAAC;EAKD,MAAM,KAAK,6BAA6B,qBACvC,KAAK,gBACL;GAAE,GAAG;GAAY,OAAO;EAAK,GAC7B,MACA,EAAE,oBAAoB,SAAS,sBAAsB,KAAK,CAC3D;EAEA,IAAI,YAAY,QACf,KAAK,WAAW;CAElB;CAEA,MACM,kBAAkB,YAAwE;EAG/F,MAAM,SAAS,+BAA+B,KAAK,cAAc,IAAI,YAAY,SAAS,KAAA;EAC1F,MAAM,aAAa,KAAK,6BAA6B,cAAc,KAAK,gBAAgB,EAC/E,OACT,CAAC;EAED,MAAM,KAAK,6BAA6B,wBAAwB,KAAK,gBAAgB,KAAA,GAAW,MAAM;EAEtG,IAAI,YAAY,QACf,KAAK,WAAW;CAElB;CAEA,MACM,WACL,YACA,SACqD;EACrD,IAAI;EACJ,IAAI;EACJ,IAAI,SAAS,iBAGZ,MAAM,KAAK,6BAA6B,qBACvC,KAAK,gBACL;GAAE,GAAG;GAAY,OAAO;EAAK,GAC7B,IACD;OACM;GACN,UAAU,MAAM,KAAK,6BAA6B,iBACjD,KAAK,gBACL,SAAS,OAAO;IAAE,GAAG;IAAY,OAAO;GAAK,IAAI,UAClD;GACA,gBAAgB,SAAS;EAC1B;EAEA,MAAM,mBAAmB,SAAS,aAAa,QAAQ,IAAI,KAAK,QAAQ,SAAS,CAAC,CAAC,QAAQ,IAAI,KAAK,IAAI;EAIxG,MAAM,uBAAuB,SAAS,yBAAyB,QAAQ,WAAW;EAClF,IAAI,WAAW,QAAQ,oBAAoB,sBAAsB;GAChE,UAAU,MAAM,KAAK,cAAc,YAAY;IAC9C,GAAG;IACH,kBAAkB;IACI;GACvB,CAAC;GAED,IAAI,WAAW,MACd,MAAM,KAAK,6BAA6B,aAAa,KAAK,gBAAgB,OAAO;EAEnF;EAEA,IAAI,kBAAkB,SAAS,aAC9B,KAAK,WAAW;EAGjB,OAAO;CACR;CAOA,aAA6B;EAC5B,qBAAqB,KAAK,aAAa,KAAK,CAAC;CAC9C;AACD;AAhGE,WAAA,CAAA,MAAM,CAAA,GAAA,sCAAA,WAAA,iBAAA,IAAA;AAyBN,WAAA,CAAA,MAAM,CAAA,GAAA,sCAAA,WAAA,qBAAA,IAAA;AAgBN,WAAA,CAAA,MAAM,CAAA,GAAA,sCAAA,WAAA,cAAA,IAAA;AAyDR,IAAa,yCAAb,cAEU,sCAA0C;CAIjC;CAHlB,YACC,uBACA,8BACA,iBACC;EACD,MAAM,uBAAuB,4BAA4B;EAFxC,KAAA,kBAAA;CAGlB;CAEA,IAAc,iBAAqB;EAClC,OAAO,KAAK;CACb;CAEA,MAAyB,cACxB,YACA,SACqD;EACrD,IAAI,SAAS,iBAAiB;GAC7B,IAAK,MAAM,KAAK,uBAAuB,MAAO,WAC7C;GAGD,KAAU,oBAAoB,OAAO,SAAS,MAAM;GACpD;EACD;EAOA,IAAI,UACH,SAAS,oBACT,SAAS,wBACT,SAAS,kBACT,SAAS,mBACT,SAAS,OACN,MAAM,KAAK,gBAAgB,YAAY,EAAE,sBAAsB,SAAS,qBAAqB,CAAC,IAC9F,KAAA;EAGJ,IAD4B,SAAS,kBAAkB,WAAW,MACzC;GAExB,IAAI,CAAC,MADmB,KAAK,oBAAoB,MAAM,SAAS,MAAM,GACtD,OAAO,KAAA;GAIvB,UAAU,MAAM,KAAK,WAAW,YAAY,EAAE,QAAQ,SAAS,OAAO,CAAC;EACxE;EACA,OAAO;CACR;CAEA,oBAA4B,iBAA0B,QAA+C;EAGpG,MAAM,KAAK,KAAK;EAChB,IAAI,8BAA8B,EAAE,GACnC,OAAO,KAAK,sBAAsB,yBACjC;GAAE,gBAAgB,CAAC,EAAE;GAAoB;GAAiB,aAAa;EAAK,GAC5E;GACC,QAAQ,UAAU;GAClB,QAAQ;IACP,QAAQ;IACR,aAAa;GACd;EACD,CACD;EAGD,OAAO,QAAQ,QAAQ,KAAK;CAC7B;CAEA,MAAc,gBACb,YACA,SACqD;EAErD,IAAI,EADc,MAAM,KAAK,sBAAsB,IAAI,QAAQ,WAAW,KAAM,OACjE,OAAO,KAAA;EAEtB,MAAM,oBAAoB,KAAK,sBAAsB;EAMrD,MAAM,eACL,WAAW,iBACV,+BAA+B,KAAK,cAAc,IAChD,KAAK,6BAA6B,0BAClC,KAAK,gBACL,WAAW,QACX,IACD,IACC,KAAA;EAaJ,MAAM,mBAAmB,KAAK,6BAA6B,8BAC1D,KAAK,gBACL,cACA,WAAW,MACZ;EACA,MAAM,oBAAoB,YACzB,WAAW,QACX,+BAA+B,KAAK,cAAc,MACjD,CAAC,oBAAoB,QAAQ,QAAQ,QAAQ,KAAK,CAAC,MACpD,CAAC,qBAAqB,QAAQ,QAAQ,WAAW,MAAM;EAExD,IAAI,UAAU,MAAM,kBAAkB,qBAAqB,KAAK,gBAAgB,KAAA,GAAW,YAAY;EACvG,IAAI,iBAAiB,OAAO,GAAG,OAAO,KAAA;EAKtC,IAAI,SAAS,cAAc,KAAK,oCAAoC,KAAK,cAAc,GACtF,QAAQ,YAAY;EASrB,IAAI,WAAW,SAAS,QAAQ,YAAY,MAAM,SAAS,uBAAuB;GACjF,UAAU,MAAM,kBAAkB,qBACjC,KAAK,gBACL,QAAQ,aACR,YACD;GACA,IAAI,iBAAiB,OAAO,GAAG,OAAO,KAAA;EACvC;EAEA,IAAI,CAAC,SAAS,OAAO,KAAA;EAErB,IAAI;EAGJ,IAAI,2BAA2B,KAAK,QAAQ,MAAM,GACjD,IAAI;GACH,kBAAkB,IAAI,IAAI,QAAQ,MAAM,CAAC,CAAC;EAC3C,QAAQ;GACP,kBAAkB,KAAA;EACnB;EAGD,OAAO;GAGN,IAAI,QAAQ,MAAM,KAAK,6BAA6B,oBAAoB,KAAK,gBAAgB,UAAU;GACvG,aAAa,QAAQ;GACrB,QAAQ,WAAW;GACnB,SAAS;IACR,IAAI;IACJ,OAAO;GACR;GACA,OAAO;GACP,MAAM,QAAQ;GACd,WAAW,IAAI,KAAK,QAAQ,YAAY,MAAO,KAAK,IAAI,CAAC;GAEzD,QAAQ,WAAW;GAMnB,SACC,QAAQ,UACR,KAAK,6BAA6B,qBACjC,KAAK,gBACL,cACA,WAAW,MACZ;GACD,UAAU,mBAAmB,KAAA;GAE7B,QAAQ,QAAQ;EACjB;CACD;CAEA,MAAc,yBAAkE;EAE/E,IAAI,EADc,MAAM,KAAK,sBAAsB,IAAI,QAAQ,WAAW,KAAM,OACjE,OAAO;EAGtB,OAAQ,MADkB,KAAK,sBAAsB,kBACrB,WAAW,KAAK,cAAc,IAAK,YAAY;CAChF;AACD"}
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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"}
@@ -559,8 +559,10 @@ var IntegrationService = class {
559
559
  connectionId,
560
560
  cloud: true
561
561
  };
562
- await authProvider?.deleteSession(descriptor);
563
- await authProvider?.getSession(descriptor, { sync: true });
562
+ if (await authProvider?.getSession(descriptor, {
563
+ sync: true,
564
+ refetch: true
565
+ }) == null) await authProvider?.deleteSession(descriptor);
564
566
  } else await integration.syncCloudConnection("connected", true);
565
567
  } catch {}
566
568
  }
@@ -932,6 +934,7 @@ var IntegrationService = class {
932
934
  const syncedPrimaryIdsByDomain = /* @__PURE__ */ new Map();
933
935
  const existingById = new Map(this.configuredIntegrationService.getConfigured(id, { cloud: true }).map((c) => [c.id, c]));
934
936
  const repointedDomains = /* @__PURE__ */ new Set();
937
+ const signOutMark = this.configuredIntegrationService.getSignOutMark();
935
938
  const preparedConnections = await Promise.all(identified.map(async (connection) => {
936
939
  const host = hostFromDomain(connection.domain);
937
940
  if (isSelfManagedHostIntegrationId(id) && !host) {
@@ -974,7 +977,7 @@ var IntegrationService = class {
974
977
  }));
975
978
  for (const prepared of preparedConnections) {
976
979
  if (prepared == null) continue;
977
- if (prepared.kind === "fetched") await this.configuredIntegrationService.storeSession(id, prepared.providerSession);
980
+ if (prepared.kind === "fetched" && !await this.configuredIntegrationService.storeSession(id, prepared.providerSession, { signOutMark })) continue;
978
981
  syncedIds.add(prepared.connection.id);
979
982
  if (prepared.connection.primary) {
980
983
  const domain = isSelfManagedHostIntegrationId(id) ? prepared.host : void 0;