@gitkraken/core-gitlens 0.10.1 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -1
- package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts +40 -27
- package/dist/plus/integrations/authentication/configuredIntegrationService.js +92 -22
- package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +10 -6
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +1 -1
- package/dist/plus/integrations/collectionMetadata.js +19 -8
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/errors.d.ts +2 -0
- package/dist/plus/integrations/errors.js +45 -1
- package/dist/plus/integrations/errors.js.map +1 -1
- package/dist/plus/integrations/integrationService.js +2 -1
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +18 -16
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +46 -8
- package/dist/plus/integrations/models/integration.js +159 -54
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.js +2 -6
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +1 -2
- package/dist/plus/integrations/providers/azureDevOps.js +12 -5
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/github.js +1 -4
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
- package/dist/plus/integrations/providers/jira.js +1 -0
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.js +1 -0
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +0 -1
- package/dist/plus/integrations/results.d.ts +8 -5
- package/dist/plus/integrations/results.js +3 -1
- package/dist/plus/integrations/results.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integrationAuthenticationProvider.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/integrationAuthenticationProvider.ts"],"sourcesContent":["import { trace } from '@gitlens/utils/decorators/log.js';\nimport type { Disposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { Sources } from '../telemetry.js';\nimport { areDomainsOnSameHost } from '../utils/domain.utils.js';\nimport { isNonExpiringZeroTokenIntegrationId, isSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ConfiguredIntegrationService } from './configuredIntegrationService.js';\nimport type { IntegrationAuthenticationService } from './integrationAuthenticationService.js';\nimport type { ProviderAuthenticationSession } from './models.js';\nimport { isSupportedCloudIntegrationId } from './models.js';\n\nconst maxSmallIntegerV8 = 2 ** 30 - 1; // Max number that can be stored in V8's smis (small integers)\n\nexport interface IntegrationAuthenticationProviderDescriptor {\n\tid: IntegrationIds;\n\tscopes: string[];\n}\n\nexport interface IntegrationAuthenticationSessionDescriptor {\n\tdomain: string;\n\tscopes: string[];\n\t/** When set, reads/deletes only the matching storage variant. */\n\tcloud?: boolean;\n\t/**\n\t * Targets a specific connection when a provider has multiple accounts connected. When omitted,\n\t * operations resolve to the provider's primary connection (see\n\t * {@link ConfiguredIntegrationService.resolveConnectionId}).\n\t */\n\tconnectionId?: string;\n\t[key: string]: unknown;\n}\n\n/**\n * Options for {@link IntegrationAuthenticationProvider.getSession}. The two variants keep `sync` exclusive\n * with the interactive flags: a sync resolves whatever the cloud already has, so it must never open a connect\n * window.\n *\n * `refreshRejectedToken` marks the stored token as known-refused by the provider (an `AuthenticationError` on\n * a previous read), which forces a cloud `/refresh` even when the token's expiry still claims it is valid —\n * the case a purely time-based refresh cannot see. The refresh token itself never reaches the client: the GK\n * cloud performs the exchange server-side.\n *\n * `refetch` (sync only) fetches a fresh token instead of serving the stored one, WITHOUT deleting the stored one\n * first: it is replaced only once the fetch returns a session. A forced re-sync used to delete it up front, so a\n * fetch that failed transiently (a GK API 429 or 5xx) left the connection with no token at all, and every read\n * until the next good sync reported it as gone (gitkraken/kepler#3546).\n */\nexport type GetSessionOptions =\n\t| {\n\t\t\tcreateIfNeeded?: boolean;\n\t\t\tforceNewSession?: boolean;\n\t\t\tsync?: never;\n\t\t\trefetch?: never;\n\t\t\trefreshRejectedToken?: boolean;\n\t\t\tsource?: Sources;\n\t }\n\t| {\n\t\t\tcreateIfNeeded?: never;\n\t\t\tforceNewSession?: never;\n\t\t\tsync: boolean;\n\t\t\trefetch?: boolean;\n\t\t\trefreshRejectedToken?: boolean;\n\t\t\tsource?: Sources;\n\t };\n\n/**\n * What {@link IntegrationAuthenticationProviderBase.getSession} passes down once it has decided the stored\n * session can't be served as-is. `refreshIfExpired` is its own conclusion, not a caller's option.\n */\ntype GetNewSessionOptions = GetSessionOptions & { refreshIfExpired?: boolean };\n\nexport interface IntegrationAuthenticationProvider extends Disposable {\n\t/**\n\t * Clears the stored secret for one connection only (never the whole provider). Unlike\n\t * {@link deleteAllSessions}, this by default leaves the descriptor in `integrations:configured` — its\n\t * primary caller is a forced re-sync, which needs `getConfigured()` to keep reporting the connection\n\t * while a fresh session is fetched to replace the deleted secret. Implementers should not treat this as\n\t * a full disconnect. Pass `preserveConfigured: false` to also drop the descriptor (e.g. when the\n\t * re-sync's replacement fetch failed and the connection should no longer be reported as connected).\n\t */\n\tdeleteSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void>;\n\tdeleteAllSessions(descriptor?: IntegrationAuthenticationSessionDescriptor): Promise<void>;\n\t/**\n\t * Resolves the session for `descriptor`, from storage when it is still usable and from the cloud\n\t * otherwise. See {@link GetSessionOptions}.\n\t */\n\tgetSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined>;\n\tget onDidChange(): Event<void>;\n}\n\nabstract class IntegrationAuthenticationProviderBase<\n\tID extends IntegrationIds = IntegrationIds,\n> implements IntegrationAuthenticationProvider {\n\tprotected readonly disposables: Disposable[] = [];\n\n\tconstructor(\n\t\tprotected readonly authenticationService: IntegrationAuthenticationService,\n\t\tprotected readonly configuredIntegrationService: ConfiguredIntegrationService,\n\t) {}\n\n\tdispose(): void {\n\t\tthis.disposables.forEach(d => d.dispose());\n\t}\n\n\tprivate readonly _onDidChange = new Emitter<void>();\n\tget onDidChange(): Event<void> {\n\t\treturn this._onDidChange.event;\n\t}\n\n\tprotected abstract get authProviderId(): ID;\n\n\t@trace()\n\tasync deleteSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void> {\n\t\tconst domain = isSelfManagedHostIntegrationId(this.authProviderId) ? descriptor?.domain : undefined;\n\t\tconst configured = this.configuredIntegrationService.getConfigured(this.authProviderId, {\n\t\t\tdomain: domain,\n\t\t});\n\n\t\t// Scope the descriptor to cloud so resolveConnectionId targets the cloud variant's id: this is a\n\t\t// cloud-only delete, and a mixed local+cloud connection whose local descriptor is primary would\n\t\t// otherwise resolve the local id and leave the cloud secret intact.\n\t\tawait this.configuredIntegrationService.deleteStoredSessions(\n\t\t\tthis.authProviderId,\n\t\t\t{ ...descriptor, cloud: true },\n\t\t\ttrue,\n\t\t\t{ preserveConfigured: options?.preserveConfigured ?? true },\n\t\t);\n\n\t\tif (configured?.length) {\n\t\t\tthis.fireChange();\n\t\t}\n\t}\n\n\t@trace()\n\tasync deleteAllSessions(descriptor?: IntegrationAuthenticationSessionDescriptor): Promise<void> {\n\t\t// Self-managed providers group every host under one provider id, so scope the clear to this\n\t\t// descriptor's host when given; for cloud providers the domain stays undefined here, clearing every account.\n\t\tconst domain = isSelfManagedHostIntegrationId(this.authProviderId) ? descriptor?.domain : undefined;\n\t\tconst configured = this.configuredIntegrationService.getConfigured(this.authProviderId, {\n\t\t\tdomain: domain,\n\t\t});\n\n\t\tawait this.configuredIntegrationService.deleteAllStoredSessions(this.authProviderId, undefined, domain);\n\n\t\tif (configured?.length) {\n\t\t\tthis.fireChange();\n\t\t}\n\t}\n\n\t@trace()\n\tasync getSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tlet session;\n\t\tlet previousToken;\n\t\tlet refetched: ProviderAuthenticationSession | undefined;\n\t\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\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\n\t\t\tif (session != null) {\n\t\t\t\tawait this.configuredIntegrationService.storeSession(this.authProviderId, session);\n\t\t\t\t// The replacement belongs to a different connection (the cloud's primary moved to another\n\t\t\t\t// account), so it was stored beside the one it replaces rather than over it. Drop the old token\n\t\t\t\t// as the up-front delete used to, or an unscoped read would keep resolving it until the next sync.\n\t\t\t\tif (refetched?.id != null && refetched.id !== session.id) {\n\t\t\t\t\tawait this.configuredIntegrationService.deleteStoredSessions(\n\t\t\t\t\t\tthis.authProviderId,\n\t\t\t\t\t\t{ ...descriptor, connectionId: refetched.id, cloud: true },\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t{ preserveConfigured: true },\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (previousToken !== session?.accessToken) {\n\t\t\tthis.fireChange();\n\t\t}\n\n\t\treturn session;\n\t}\n\n\tprotected abstract getNewSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetNewSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined>;\n\n\tprotected fireChange(): void {\n\t\tqueueMicrotask(() => this._onDidChange.fire());\n\t}\n}\n\nexport class CloudIntegrationAuthenticationProvider<\n\tID extends IntegrationIds = IntegrationIds,\n> extends IntegrationAuthenticationProviderBase<ID> {\n\tconstructor(\n\t\tauthenticationService: IntegrationAuthenticationService,\n\t\tconfiguredIntegrationService: ConfiguredIntegrationService,\n\t\tprivate readonly _authProviderId: ID,\n\t) {\n\t\tsuper(authenticationService, configuredIntegrationService);\n\t}\n\n\tprotected get authProviderId(): ID {\n\t\treturn this._authProviderId;\n\t}\n\n\tprotected override async getNewSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetNewSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tif (options?.forceNewSession) {\n\t\t\tif ((await this.disconnectCloudSession()) === 'failure') {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tvoid this.connectCloudSession(false, options?.source);\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// TODO: This is a stopgap to make sure we're not hammering the api on automatic calls to get the session.\n\t\t// Ultimately we want to timestamp calls to syncCloudIntegrations and use that to determine whether we should\n\t\t// make the call or not.\n\t\t// `refreshRejectedToken` joins the list for the same reason `refreshIfExpired` is on it: both mean the\n\t\t// session in hand is known-unusable, so skipping the cloud round trip would just re-serve it.\n\t\tlet session =\n\t\t\toptions?.refreshIfExpired ||\n\t\t\toptions?.refreshRejectedToken ||\n\t\t\toptions?.createIfNeeded ||\n\t\t\toptions?.forceNewSession ||\n\t\t\toptions?.sync\n\t\t\t\t? await this.getCloudSession(descriptor, { refreshRejectedToken: options?.refreshRejectedToken })\n\t\t\t\t: undefined;\n\n\t\tconst shouldCreateSession = options?.createIfNeeded && session == null;\n\t\tif (shouldCreateSession) {\n\t\t\tconst connected = await this.connectCloudSession(true, options?.source);\n\t\t\tif (!connected) return undefined;\n\n\t\t\t// This should get us the session we just created with connectCloudSession, because a syncCloudIntegrations run from\n\t\t\t// integration service should have resulted in it being created and stored by this provider\n\t\t\tsession = await this.getSession(descriptor, { source: options?.source });\n\t\t}\n\t\treturn session;\n\t}\n\n\tprivate connectCloudSession(skipIfConnected: boolean, source: Sources | undefined): Promise<boolean> {\n\t\t// Capture in a const so the type guard narrows it to `SupportedCloudIntegrationIds` (a getter\n\t\t// access wouldn't narrow), letting us call the service directly without a cast.\n\t\tconst id = this.authProviderId;\n\t\tif (isSupportedCloudIntegrationId(id)) {\n\t\t\treturn this.authenticationService.connectCloudIntegrations(\n\t\t\t\t{ integrationIds: [id], skipIfConnected: skipIfConnected, skipPreSync: true },\n\t\t\t\t{\n\t\t\t\t\tsource: source ?? 'integrations',\n\t\t\t\t\tdetail: {\n\t\t\t\t\t\taction: 'connect',\n\t\t\t\t\t\tintegration: id,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\n\t\treturn Promise.resolve(false);\n\t}\n\n\tprivate async getCloudSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: { refreshRejectedToken?: boolean },\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tconst loggedIn = (await this.authenticationService.ctx.account.getAccount()) != null;\n\t\tif (!loggedIn) return undefined;\n\n\t\tconst cloudIntegrations = this.authenticationService.cloudIntegrations;\n\t\t// An unscoped descriptor (no explicit connectionId) would fetch the provider-global primary via\n\t\t// `v1/provider-tokens/<provider>`. For a self-managed provider spanning multiple hosts that primary\n\t\t// can belong to a different host, so a forced sync of host A would hydrate host B's token here (before\n\t\t// reconcile corrects storage). Scope to this host's own configured connection when we have one; fall\n\t\t// through to the provider-scoped path only when nothing is configured yet (legacy/first sync).\n\t\tconst connectionId =\n\t\t\tdescriptor.connectionId ??\n\t\t\t(isSelfManagedHostIntegrationId(this.authProviderId)\n\t\t\t\t? this.configuredIntegrationService.getConfiguredConnectionId(\n\t\t\t\t\t\tthis.authProviderId,\n\t\t\t\t\t\tdescriptor.domain,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t)\n\t\t\t\t: undefined);\n\t\t// Refuses a session issued for a DIFFERENT host than the one this connection is keyed by, which would\n\t\t// otherwise let host B's token serve host A.\n\t\t//\n\t\t// A session that names no host is accepted ONLY when it was fetched by a connection id WE have stored\n\t\t// for this host. That descriptor is what ties the token to the host, so an absent domain then means the\n\t\t// backend did not say rather than that the token belongs elsewhere, and refusing it would strand the\n\t\t// refresh.\n\t\t//\n\t\t// A non-null id is not enough on its own: `descriptor.connectionId` can come from the caller, and with\n\t\t// no id at all the fetch goes to the provider-GLOBAL primary endpoint whose token can belong to any\n\t\t// configured host. In both of those cases a domainless response must be refused, or a token would be\n\t\t// bound to — and sent to — a host it was never issued for.\n\t\tconst scopedToThisHost = this.configuredIntegrationService.isConnectionConfiguredForHost(\n\t\t\tthis.authProviderId,\n\t\t\tconnectionId,\n\t\t\tdescriptor.domain,\n\t\t);\n\t\tconst isForAnotherHost = (session: { domain: string } | undefined) =>\n\t\t\tsession != null &&\n\t\t\tisSelfManagedHostIntegrationId(this.authProviderId) &&\n\t\t\t(!scopedToThisHost || Boolean(session.domain?.trim())) &&\n\t\t\t!areDomainsOnSameHost(session.domain, descriptor.domain);\n\n\t\tlet session = await cloudIntegrations.getConnectionSession(this.authProviderId, undefined, connectionId);\n\t\tif (isForAnotherHost(session)) return undefined;\n\n\t\t// GitHub, the cloud self-managed hosts, and Trello return `expiresIn: 0` for a token that never\n\t\t// expires; left as 0 the session would be built with `expiresAt = now` and rejected as expired on the\n\t\t// next resolution. Map it to the maximum expiry so it isn't refreshed frequently.\n\t\tif (session?.expiresIn === 0 && isNonExpiringZeroTokenIntegrationId(this.authProviderId)) {\n\t\t\tsession.expiresIn = maxSmallIntegerV8; // maximum expiration length\n\t\t}\n\n\t\t// `expiresIn < 60` is a PREDICTION that the token is about to lapse, and it is the only trigger the\n\t\t// refresh used to have. It cannot see a token the provider has already refused while the backend\n\t\t// still believes it is valid — a revoked grant, retired scopes, an app removed from the org — where\n\t\t// `expiresIn` stays high and the doomed token is re-sent on every read. `refreshRejectedToken` is\n\t\t// the observed counterpart: the caller saw the provider reject this exact token, so refresh it\n\t\t// regardless of what its expiry claims.\n\t\tif (session != null && (session.expiresIn < 60 || options?.refreshRejectedToken)) {\n\t\t\tsession = await cloudIntegrations.getConnectionSession(\n\t\t\t\tthis.authProviderId,\n\t\t\t\tsession.accessToken,\n\t\t\t\tconnectionId,\n\t\t\t);\n\t\t\tif (isForAnotherHost(session)) return undefined;\n\t\t}\n\n\t\tif (!session) return undefined;\n\n\t\tlet sessionProtocol;\n\t\t// Only derive a protocol from a domain carrying an explicit scheme; a bare `host:port` (e.g. a\n\t\t// self-managed `ghe.example.com:8443`) parses the host as the protocol, corrupting the value.\n\t\tif (/^[a-z][a-z\\d+\\-.]*:\\/\\//i.test(session.domain)) {\n\t\t\ttry {\n\t\t\t\tsessionProtocol = new URL(session.domain).protocol;\n\t\t\t} catch {\n\t\t\t\tsessionProtocol = undefined;\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\t// Prefer the backend's per-connection token id (multi-account); fall back to the resolved\n\t\t\t// primary/legacy connection id so existing single-connection storage keys are preserved.\n\t\t\tid: session.id ?? this.configuredIntegrationService.resolveConnectionId(this.authProviderId, descriptor),\n\t\t\taccessToken: session.accessToken,\n\t\t\tscopes: descriptor.scopes,\n\t\t\taccount: {\n\t\t\t\tid: '',\n\t\t\t\tlabel: '',\n\t\t\t},\n\t\t\tcloud: true,\n\t\t\ttype: session.type,\n\t\t\texpiresAt: new Date(session.expiresIn * 1000 + Date.now()),\n\t\t\t// Note: do not use the session's domain, because the format is different than in our model\n\t\t\tdomain: descriptor.domain,\n\t\t\t// That format difference is exactly what `baseUrl` is for: the descriptor's domain is the\n\t\t\t// normalized host this connection is keyed by, so a context path only survives on the wire value.\n\t\t\t// When the backend reports no domain at all, KEEP the address already configured rather than\n\t\t\t// writing `undefined` — `writeSecret` mirrors this onto the descriptor, so falling back to the\n\t\t\t// bare host here would drop a context path the user never changed on the next refresh.\n\t\t\tbaseUrl:\n\t\t\t\tsession.domain ||\n\t\t\t\tthis.configuredIntegrationService.getConfiguredBaseUrl(\n\t\t\t\t\tthis.authProviderId,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\tdescriptor.domain,\n\t\t\t\t),\n\t\t\tprotocol: sessionProtocol ?? undefined,\n\t\t\t// Carried for providers whose client needs an app key alongside the token (e.g. Trello).\n\t\t\tappKey: session.appKey,\n\t\t};\n\t}\n\n\tprivate async disconnectCloudSession(): Promise<'skip' | 'success' | 'failure'> {\n\t\tconst loggedIn = (await this.authenticationService.ctx.account.getAccount()) != null;\n\t\tif (!loggedIn) return 'skip';\n\n\t\tconst cloudIntegrations = this.authenticationService.cloudIntegrations;\n\t\treturn (await cloudIntegrations.disconnect(this.authProviderId)) ? 'success' : 'failure';\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"integrationAuthenticationProvider.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/integrationAuthenticationProvider.ts"],"sourcesContent":["import { trace } from '@gitlens/utils/decorators/log.js';\nimport type { Disposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { Sources } from '../telemetry.js';\nimport { areDomainsOnSameHost } from '../utils/domain.utils.js';\nimport { isNonExpiringZeroTokenIntegrationId, isSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ConfiguredIntegrationService } from './configuredIntegrationService.js';\nimport type { IntegrationAuthenticationService } from './integrationAuthenticationService.js';\nimport type { ProviderAuthenticationSession } from './models.js';\nimport { isSupportedCloudIntegrationId } from './models.js';\n\nconst maxSmallIntegerV8 = 2 ** 30 - 1; // Max number that can be stored in V8's smis (small integers)\n\nexport interface IntegrationAuthenticationProviderDescriptor {\n\tid: IntegrationIds;\n\tscopes: string[];\n}\n\nexport interface IntegrationAuthenticationSessionDescriptor {\n\tdomain: string;\n\tscopes: string[];\n\t/** When set, reads/deletes only the matching storage variant. */\n\tcloud?: boolean;\n\t/**\n\t * Targets a specific connection when a provider has multiple accounts connected. When omitted,\n\t * operations resolve to the provider's primary connection (see\n\t * {@link ConfiguredIntegrationService.resolveConnectionId}).\n\t */\n\tconnectionId?: string;\n\t[key: string]: unknown;\n}\n\n/**\n * Options for {@link IntegrationAuthenticationProvider.getSession}. The two variants keep `sync` exclusive\n * with the interactive flags: a sync resolves whatever the cloud already has, so it must never open a connect\n * window.\n *\n * `refreshRejectedToken` marks the stored token as known-refused by the provider (an `AuthenticationError` on\n * a previous read), which forces a cloud `/refresh` even when the token's expiry still claims it is valid —\n * the case a purely time-based refresh cannot see. The refresh token itself never reaches the client: the GK\n * cloud performs the exchange server-side.\n *\n * `refetch` (sync only) fetches a fresh token instead of serving the stored one, WITHOUT deleting the stored one\n * first: it is replaced only once the fetch returns a session. A forced re-sync used to delete it up front, so a\n * fetch that failed transiently (a GK API 429 or 5xx) left the connection with no token at all, and every read\n * until the next good sync reported it as gone (gitkraken/kepler#3546).\n */\nexport type GetSessionOptions =\n\t| {\n\t\t\tcreateIfNeeded?: boolean;\n\t\t\tforceNewSession?: boolean;\n\t\t\tsync?: never;\n\t\t\trefetch?: never;\n\t\t\trefreshRejectedToken?: boolean;\n\t\t\tsource?: Sources;\n\t }\n\t| {\n\t\t\tcreateIfNeeded?: never;\n\t\t\tforceNewSession?: never;\n\t\t\tsync: boolean;\n\t\t\trefetch?: boolean;\n\t\t\trefreshRejectedToken?: boolean;\n\t\t\tsource?: Sources;\n\t };\n\n/**\n * What {@link IntegrationAuthenticationProviderBase.getSession} passes down once it has decided the stored\n * session can't be served as-is. `refreshIfExpired` is its own conclusion, not a caller's option.\n */\ntype GetNewSessionOptions = GetSessionOptions & { refreshIfExpired?: boolean };\n\nexport interface IntegrationAuthenticationProvider extends Disposable {\n\t/**\n\t * Clears the stored secret for one connection only (never the whole provider). Unlike\n\t * {@link deleteAllSessions}, this by default leaves the descriptor in `integrations:configured` — its\n\t * primary caller is a forced re-sync, which needs `getConfigured()` to keep reporting the connection\n\t * while a fresh session is fetched to replace the deleted secret. Implementers should not treat this as\n\t * a full disconnect. Pass `preserveConfigured: false` to also drop the descriptor (e.g. when the\n\t * re-sync's replacement fetch failed and the connection should no longer be reported as connected).\n\t */\n\tdeleteSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void>;\n\tdeleteAllSessions(descriptor?: IntegrationAuthenticationSessionDescriptor): Promise<void>;\n\t/**\n\t * Resolves the session for `descriptor`, from storage when it is still usable and from the cloud\n\t * otherwise. See {@link GetSessionOptions}.\n\t */\n\tgetSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined>;\n\tget onDidChange(): Event<void>;\n}\n\nabstract class IntegrationAuthenticationProviderBase<\n\tID extends IntegrationIds = IntegrationIds,\n> implements IntegrationAuthenticationProvider {\n\tprotected readonly disposables: Disposable[] = [];\n\n\tconstructor(\n\t\tprotected readonly authenticationService: IntegrationAuthenticationService,\n\t\tprotected readonly configuredIntegrationService: ConfiguredIntegrationService,\n\t) {}\n\n\tdispose(): void {\n\t\tthis.disposables.forEach(d => d.dispose());\n\t}\n\n\tprivate readonly _onDidChange = new Emitter<void>();\n\tget onDidChange(): Event<void> {\n\t\treturn this._onDidChange.event;\n\t}\n\n\tprotected abstract get authProviderId(): ID;\n\n\t@trace()\n\tasync deleteSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void> {\n\t\tconst domain = isSelfManagedHostIntegrationId(this.authProviderId) ? descriptor?.domain : undefined;\n\t\tconst configured = this.configuredIntegrationService.getConfigured(this.authProviderId, {\n\t\t\tdomain: domain,\n\t\t});\n\n\t\t// Scope the descriptor to cloud so resolveConnectionId targets the cloud variant's id: this is a\n\t\t// cloud-only delete, and a mixed local+cloud connection whose local descriptor is primary would\n\t\t// otherwise resolve the local id and leave the cloud secret intact.\n\t\tawait this.configuredIntegrationService.deleteStoredSessions(\n\t\t\tthis.authProviderId,\n\t\t\t{ ...descriptor, cloud: true },\n\t\t\ttrue,\n\t\t\t{ preserveConfigured: options?.preserveConfigured ?? true },\n\t\t);\n\n\t\tif (configured?.length) {\n\t\t\tthis.fireChange();\n\t\t}\n\t}\n\n\t@trace()\n\tasync deleteAllSessions(descriptor?: IntegrationAuthenticationSessionDescriptor): Promise<void> {\n\t\t// Self-managed providers group every host under one provider id, so scope the clear to this\n\t\t// descriptor's host when given; for cloud providers the domain stays undefined here, clearing every account.\n\t\tconst domain = isSelfManagedHostIntegrationId(this.authProviderId) ? descriptor?.domain : undefined;\n\t\tconst configured = this.configuredIntegrationService.getConfigured(this.authProviderId, {\n\t\t\tdomain: domain,\n\t\t});\n\n\t\tawait this.configuredIntegrationService.deleteAllStoredSessions(this.authProviderId, undefined, domain);\n\n\t\tif (configured?.length) {\n\t\t\tthis.fireChange();\n\t\t}\n\t}\n\n\t@trace()\n\tasync getSession(\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\toptions?: GetSessionOptions,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tlet session;\n\t\tlet previousToken;\n\t\tlet refetched: ProviderAuthenticationSession | undefined;\n\t\tlet signOutMark: number;\n\t\tif (options?.forceNewSession) {\n\t\t\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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"}
|
|
@@ -66,7 +66,7 @@ export interface ProviderScopeFailure extends CollectionScopeFailure {
|
|
|
66
66
|
* is the case this failure exists to tell apart, and that sentence says the opposite.
|
|
67
67
|
*/
|
|
68
68
|
export declare function toCollectionScopeFailure(scope: CollectionScopeFailure['scope'], ex: unknown): ProviderScopeFailure;
|
|
69
|
-
export declare function toCollectionFailureError(failure:
|
|
69
|
+
export declare function toCollectionFailureError(failure: ProviderScopeFailure, tokenWithInfo: TokenWithInfo): Error;
|
|
70
70
|
/**
|
|
71
71
|
* Whether `metadata` reports authentication failures only for scopes below the provider, with none for the
|
|
72
72
|
* connection itself. A credential revoked since a provider cached its discovery produces exactly this shape,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuthenticationError, RequestClientError, RequestNotFoundError, RequestRateLimitError } from "../../git/errors.js";
|
|
2
|
-
import { getResponseHeader, isRateLimitResponse } from "./errors.js";
|
|
2
|
+
import { getResponseHeader, isProviderUnreachableError, isRateLimitResponse } from "./errors.js";
|
|
3
3
|
import { getProviderResponseBodyMessage } from "./providers/providerErrors.js";
|
|
4
4
|
import { appendDedupedWarning, providerWarningMessage } from "./results.js";
|
|
5
5
|
import { isInvalidRequestError, isUnsupportedSortError } from "@gitkraken/provider-apis";
|
|
@@ -82,13 +82,15 @@ function decodeServiceError(value) {
|
|
|
82
82
|
* is the case this failure exists to tell apart, and that sentence says the opposite.
|
|
83
83
|
*/
|
|
84
84
|
function toCollectionScopeFailure(scope, ex) {
|
|
85
|
+
const kind = toCollectionFailureKind(ex);
|
|
85
86
|
const refusal = toProviderRefusal(ex);
|
|
86
87
|
const message = refusal != null ? refusal.detail ?? providerWarningMessage(ex.original) : void 0;
|
|
87
88
|
return {
|
|
88
89
|
scope,
|
|
89
|
-
kind
|
|
90
|
+
kind,
|
|
90
91
|
...message ? { message } : ex instanceof Error && ex.message ? { message: ex.message } : {},
|
|
91
|
-
...refusal != null ? { refusal } : {}
|
|
92
|
+
...refusal != null ? { refusal } : {},
|
|
93
|
+
...kind === "provider" && isProviderUnreachableError(ex) ? { cause: { reason: "unreachable" } } : {}
|
|
92
94
|
};
|
|
93
95
|
}
|
|
94
96
|
function collectionFailureKindToWarningKind(kind) {
|
|
@@ -121,9 +123,15 @@ function toCollectionFailureWarningKind(failure) {
|
|
|
121
123
|
default: return mapped;
|
|
122
124
|
}
|
|
123
125
|
}
|
|
126
|
+
function toCollectionFailureCause(failure, kind) {
|
|
127
|
+
return failure.cause ?? (kind === "other" && isProviderUnreachableError({
|
|
128
|
+
...failure,
|
|
129
|
+
status: getCollectionFailureStatus(failure.message)
|
|
130
|
+
}) ? { reason: "unreachable" } : void 0);
|
|
131
|
+
}
|
|
124
132
|
function toCollectionFailureError(failure, tokenWithInfo) {
|
|
125
|
-
const error = new Error(failure.message ?? "Provider request failed");
|
|
126
133
|
const kind = toCollectionFailureWarningKind(failure);
|
|
134
|
+
const error = new Error(failure.message ?? "Provider request failed", { cause: toCollectionFailureCause(failure, kind) });
|
|
127
135
|
const { accessToken, ...tokenInfo } = tokenWithInfo;
|
|
128
136
|
if (kind === "auth") return new AuthenticationError(tokenInfo, error.message, error);
|
|
129
137
|
if (kind === "rate-limit") return new RequestRateLimitError(error, accessToken, void 0);
|
|
@@ -146,6 +154,7 @@ function causeText(cause) {
|
|
|
146
154
|
case "oauth-app-not-allowed": return "the organization does not allow third-party OAuth apps";
|
|
147
155
|
case "access-denied": return "the account has no access to it";
|
|
148
156
|
case "conditional-access": return "a Conditional Access policy blocked the request";
|
|
157
|
+
case "unreachable": return "the provider server could not be reached";
|
|
149
158
|
default:
|
|
150
159
|
cause.reason;
|
|
151
160
|
return "the provider refused the request";
|
|
@@ -309,19 +318,21 @@ function assessCollectionMetadata(providerId, domain, connectionId, metadata) {
|
|
|
309
318
|
const failures = metadata.failures ?? [];
|
|
310
319
|
for (const failure of failures) {
|
|
311
320
|
const kind = toCollectionFailureWarningKind(failure);
|
|
321
|
+
const cause = toCollectionFailureCause(failure, kind);
|
|
312
322
|
const scope = failure.credentialRefused ? void 0 : toProviderWarningScope(failure.scope);
|
|
313
323
|
appendDedupedWarning(warnings, {
|
|
314
324
|
providerId,
|
|
315
325
|
domain,
|
|
316
326
|
connectionId,
|
|
317
|
-
message: collectionFailureMessage(
|
|
327
|
+
message: collectionFailureMessage({
|
|
318
328
|
...failure,
|
|
319
|
-
|
|
320
|
-
|
|
329
|
+
cause,
|
|
330
|
+
...failure.credentialRefused ? { scope: void 0 } : {}
|
|
331
|
+
}),
|
|
321
332
|
kind,
|
|
322
333
|
isAuth: kind === "auth",
|
|
323
334
|
...scope != null ? { scope } : {},
|
|
324
|
-
...
|
|
335
|
+
...cause != null ? { cause: { ...cause } } : {}
|
|
325
336
|
});
|
|
326
337
|
}
|
|
327
338
|
const omissions = metadata.omissions ?? [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collectionMetadata.js","names":[],"sources":["../../../../plus/integrations/src/collectionMetadata.ts"],"sourcesContent":["import type {\n\tCollectionMetadata,\n\tCollectionOmission,\n\tCollectionScope,\n\tCollectionScopeFailure,\n} from '@gitkraken/provider-apis';\n// Matched by its `code` discriminator rather than by `instanceof`: the SDK ships one bundle per entry point, so\n// the class reached through the root is not the same object as the one reached through `/providers`.\nimport { isInvalidRequestError, isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport {\n\tAuthenticationError,\n\tRequestClientError,\n\tRequestNotFoundError,\n\tRequestRateLimitError,\n} from '@gitlens/git/errors.js';\nimport type { TokenWithInfo } from './authentication/models.js';\nimport type { IntegrationIds } from './constants.js';\nimport type { ResponseHeaders } from './errors.js';\nimport { getResponseHeader, isRateLimitResponse } from './errors.js';\nimport { getProviderResponseBodyMessage } from './providers/providerErrors.js';\nimport type {\n\tProviderWarning,\n\tProviderWarningCause,\n\tProviderWarningOmission,\n\tProviderWarningScope,\n} from './results.js';\nimport { appendDedupedWarning, providerWarningMessage } from './results.js';\n\n/**\n * Re-throws an error that is a fact about the CALL rather than about one scope of a fan-out.\n *\n * Every provider fan-out catches per-scope rejections and records them as {@link CollectionScopeFailure}s, which is\n * right for auth, rate limits and a missing project: those really did happen to one scope, and the siblings that\n * succeeded must survive. A caller-contract error is the opposite — it is identical for every scope and was decided\n * before any request went out — so degrading it would hand back N indistinguishable failures and an empty `partial`\n * page, describing an invalid call as an incomplete read.\n *\n * Today that means the SDK's `UnsupportedSortError` and `InvalidRequestError` — the two codes it lists in its own\n * `CALLER_CONTRACT_ERROR_CODES`, both documented as \"never degraded into a `CollectionScopeFailure` or an\n * omission\". The second covers arguments rejected before the first request goes out: a project key list longer\n * than one Jira search can carry (or an empty one), a non-integer `pageSize`, an unknown current-user filter, or a\n * Jira Server read with no `baseUrl` to address. Called from every fan-out rather than spelled out in each, so a\n * fan-out added later inherits the rule instead of silently omitting it — which is exactly how Azure's\n * account-wide drain came to degrade it while its three siblings did not.\n */\nexport function throwIfCallerContractError(ex: unknown): void {\n\tif (isUnsupportedSortError(ex) || isInvalidRequestError(ex)) throw ex;\n}\n\n/**\n * Whether a failed query that covered several scopes would fail each scope's own read the same way: a rejected or\n * rate-limited token, or a call the SDK refused before sending. Anything else (a query the provider rejects because\n * one scope is gone or no longer visible, a server error) may be one scope's problem, so the scopes are worth\n * reading on their own.\n */\nexport function failsEveryScope(ex: unknown): boolean {\n\tconst kind = toCollectionFailureKind(ex);\n\treturn (\n\t\tkind === 'authentication' || kind === 'rate-limit' || isUnsupportedSortError(ex) || isInvalidRequestError(ex)\n\t);\n}\n\n/**\n * Maps a caught GitLens request error to the SDK collection failure vocabulary used inside provider fan-outs.\n */\nexport function toCollectionFailureKind(ex: unknown): CollectionScopeFailure['kind'] {\n\tif (ex instanceof AuthenticationError) return 'authentication';\n\tif (ex instanceof RequestRateLimitError) return 'rate-limit';\n\tif (ex instanceof RequestNotFoundError) return 'not-found';\n\treturn 'provider';\n}\n\n/**\n * What an authentication refusal's response said, kept on the failure so the provider can name its cause once the\n * credential behind it is confirmed (see `IntegrationBase.confirmScopedAuthFailures`). Only these facts are kept,\n * never the response: its headers carry the provider's session cookies.\n */\nexport interface ProviderRefusal {\n\tstatus: number;\n\t/** The provider's own explanation: Azure DevOps' `X-TFS-ServiceError`, or the error body's message. */\n\tdetail?: string;\n\t/** The provider's error type, when the body names one (Azure DevOps `typeKey`). */\n\ttypeKey?: string;\n\t/**\n\t * The OAuth app the refused token belongs to, when the provider says (GitHub's `X-OAuth-Client-Id`). Not a\n\t * secret: it is the app's public client id, which the page where a user asks an organization to approve the app\n\t * is addressed by.\n\t */\n\toauthClientId?: string;\n}\n\n/** A scope failure as recorded here: the SDK's shape, plus what the refusal said and, once it is known, why. */\nexport interface ProviderScopeFailure extends CollectionScopeFailure {\n\trefusal?: ProviderRefusal;\n\tcause?: ProviderWarningCause;\n\t/** The provider pinned the refusal on the credential itself, so it is published for the connection, unscoped. */\n\tcredentialRefused?: true;\n}\n\n/**\n * Reads the refusal off an `AuthenticationError` that carries the provider's response: the SDK adapter's plain\n * `{ status, headers, body }` (`providersApi.ts`), Octokit's `{ status, headers, data }` (the GitHub client's REST\n * reads), or the `Response` of a `ProviderFetchError`, whose body was already read into its message.\n */\nfunction toProviderRefusal(ex: unknown): ProviderRefusal | undefined {\n\tif (!(ex instanceof AuthenticationError)) return undefined;\n\n\tconst response = (ex.original as { response?: unknown } | undefined)?.response as\n\t\t| { status?: unknown; headers?: ResponseHeaders; body?: unknown; data?: unknown }\n\t\t| undefined;\n\tif (typeof response?.status !== 'number') return undefined;\n\n\t// Only a structured body: a string one is a page (a proxy's, a sign-in form), not an explanation.\n\tconst raw = response.body ?? response.data;\n\tconst body = typeof raw === 'object' && raw != null ? raw : undefined;\n\tconst detail =\n\t\tdecodeServiceError(getResponseHeader(response.headers, 'x-tfs-serviceerror')) ??\n\t\tgetProviderResponseBodyMessage(body);\n\tconst typeKey = body != null && 'typeKey' in body ? body.typeKey : undefined;\n\tconst oauthClientId = getResponseHeader(response.headers, 'x-oauth-client-id');\n\n\treturn {\n\t\tstatus: response.status,\n\t\t...(detail ? { detail: detail } : {}),\n\t\t...(typeof typeKey === 'string' ? { typeKey: typeKey } : {}),\n\t\t...(typeof oauthClientId === 'string' && oauthClientId ? { oauthClientId: oauthClientId } : {}),\n\t};\n}\n\n/** Azure DevOps sends `X-TFS-ServiceError` percent-encoded. */\nfunction decodeServiceError(value: unknown): string | undefined {\n\tif (typeof value !== 'string' || !value) return undefined;\n\n\ttry {\n\t\treturn decodeURIComponent(value).trim() || undefined;\n\t} catch {\n\t\treturn value.trim() || undefined;\n\t}\n}\n\n/**\n * Builds a structured SDK scope failure from a caught GitLens request error.\n *\n * An authentication refusal is described in the provider's own words when it gives any, and otherwise by the\n * error the provider's response raised, sanitized like any warning's (a page becomes its status), rather than by\n * `AuthenticationError`'s generic \"credentials are either invalid or expired\": a scope refusing a sound credential\n * is the case this failure exists to tell apart, and that sentence says the opposite.\n */\nexport function toCollectionScopeFailure(scope: CollectionScopeFailure['scope'], ex: unknown): ProviderScopeFailure {\n\tconst refusal = toProviderRefusal(ex);\n\tconst message =\n\t\trefusal != null ? (refusal.detail ?? providerWarningMessage((ex as AuthenticationError).original)) : undefined;\n\treturn {\n\t\tscope: scope,\n\t\tkind: toCollectionFailureKind(ex),\n\t\t...(message ? { message: message } : ex instanceof Error && ex.message ? { message: ex.message } : {}),\n\t\t...(refusal != null ? { refusal: refusal } : {}),\n\t};\n}\n\nfunction collectionFailureKindToWarningKind(kind: CollectionScopeFailure['kind']): ProviderWarning['kind'] {\n\tswitch (kind) {\n\t\tcase 'authentication':\n\t\t\treturn 'auth';\n\t\tcase 'rate-limit':\n\t\t\treturn 'rate-limit';\n\t\tcase 'not-found':\n\t\t\treturn 'not-found';\n\t\tdefault:\n\t\t\treturn 'other';\n\t}\n}\n\nfunction getCollectionFailureStatus(message: string | undefined): number | undefined {\n\tif (message == null) return undefined;\n\n\tconst match = /\\((\\d{3})\\)/.exec(message) ?? /^(?:HTTP\\s+)?(\\d{3})\\b/i.exec(message.trim());\n\treturn match != null ? Number(match[1]) : undefined;\n}\n\nfunction toCollectionFailureWarningKind(failure: CollectionScopeFailure): ProviderWarning['kind'] {\n\tconst mapped = collectionFailureKindToWarningKind(failure.kind);\n\tif (mapped !== 'other' || (failure.kind !== 'provider' && failure.kind !== 'unknown')) return mapped;\n\n\t// provider-apis currently groups several HTTP statuses under `provider` in partial fan-outs. The custom\n\t// fetch adapter retains the status in `message`, so recover the same neutral classification the thrown-error\n\t// path exposes instead of downgrading these structured failures to `other`.\n\tconst status = getCollectionFailureStatus(failure.message);\n\tswitch (status) {\n\t\tcase 401:\n\t\t\treturn 'auth';\n\t\tcase 403:\n\t\t\treturn isRateLimitResponse({ status: status, message: failure.message }) ? 'rate-limit' : 'auth';\n\t\tcase 404:\n\t\tcase 410:\n\t\tcase 422:\n\t\t\treturn 'not-found';\n\t\tcase 429:\n\t\t\treturn 'rate-limit';\n\t\tdefault:\n\t\t\treturn mapped;\n\t}\n}\n\nexport function toCollectionFailureError(failure: CollectionScopeFailure, tokenWithInfo: TokenWithInfo): Error {\n\tconst error = new Error(failure.message ?? 'Provider request failed');\n\tconst kind = toCollectionFailureWarningKind(failure);\n\tconst { accessToken, ...tokenInfo } = tokenWithInfo;\n\tif (kind === 'auth') return new AuthenticationError(tokenInfo, error.message, error);\n\tif (kind === 'rate-limit') return new RequestRateLimitError(error, accessToken, undefined);\n\tif (kind === 'not-found') return new RequestNotFoundError(error);\n\n\tconst status = getCollectionFailureStatus(failure.message);\n\tif (kind === 'other' && status != null && status >= 400 && status < 500) {\n\t\treturn new RequestClientError(error);\n\t}\n\n\treturn error;\n}\n\n/** ` (resource r, project p, repository o/n)` for the scope IDs present; empty when the scope names none. */\nfunction collectionScopeText(scope: CollectionScope | undefined): string {\n\tconst parts: string[] = [];\n\tif (scope?.resourceId != null) {\n\t\tparts.push(`resource ${scope.resourceId}`);\n\t}\n\tif (scope?.projectId != null) {\n\t\tparts.push(`project ${scope.projectId}`);\n\t}\n\tif (scope?.repositoryId != null) {\n\t\tparts.push(`repository ${scope.repositoryId}`);\n\t}\n\n\treturn parts.length ? ` (${parts.join(', ')})` : '';\n}\n\n/** The cause in the consumer's terms, for the prose that goes with the structured {@link ProviderWarningCause}. */\nfunction causeText(cause: ProviderWarningCause): string {\n\tswitch (cause.reason) {\n\t\tcase 'oauth-app-not-allowed':\n\t\t\treturn 'the organization does not allow third-party OAuth apps';\n\t\tcase 'access-denied':\n\t\t\treturn 'the account has no access to it';\n\t\tcase 'conditional-access':\n\t\t\treturn 'a Conditional Access policy blocked the request';\n\t\tdefault:\n\t\t\tcause.reason satisfies never;\n\t\t\treturn 'the provider refused the request';\n\t}\n}\n\nfunction collectionFailureMessage(failure: ProviderScopeFailure): string {\n\tconst text = failure.cause != null ? causeText(failure.cause) : failure.message;\n\tconst detail = text != null ? `: ${text}` : '';\n\treturn `Failed to read ${failure.kind} scope${collectionScopeText(failure.scope)}${detail}`;\n}\n\n/**\n * Forwards the part of a failure's scope below the provider, so a consumer can tell \"one organization refused\n * this token\" from \"the connection's token is dead\" without parsing {@link collectionFailureMessage}.\n *\n * `providerId` is dropped: the warning already names its provider, and a scope naming nothing below it is a\n * failure of the connection itself. That is the distinction `getOrganizationsForUserResult` also draws before\n * counting an auth failure against the connection, though it reads a scope with no keys but `providerId`, so an\n * absent scope does not count there. Forwarding such a scope would make an account-wide failure look confined.\n *\n * Copied field by field, for the reason {@link toProviderWarningOmission} copies its scope.\n */\nfunction toProviderWarningScope(scope: CollectionScope | undefined): ProviderWarningScope | undefined {\n\tif (scope == null) return undefined;\n\n\tconst { resourceId, projectId, repositoryId } = scope;\n\tif (resourceId == null && projectId == null && repositoryId == null) return undefined;\n\n\treturn {\n\t\t...(resourceId != null ? { resourceId: resourceId } : {}),\n\t\t...(projectId != null ? { projectId: projectId } : {}),\n\t\t...(repositoryId != null ? { repositoryId: repositoryId } : {}),\n\t};\n}\n\n/**\n * Whether `metadata` reports authentication failures only for scopes below the provider, with none for the\n * connection itself. A credential revoked since a provider cached its discovery produces exactly this shape,\n * the same one a scope's own refusal does; `IntegrationBase.confirmScopedAuthFailures` tells them apart.\n *\n * Classified exactly as {@link assessCollectionMetadata} classifies the warnings, so it agrees with what a\n * consumer would be shown.\n */\nexport function hasOnlyScopedAuthFailures(metadata: CollectionMetadata | undefined): boolean {\n\tlet scoped = false;\n\tfor (const failure of metadata?.failures ?? []) {\n\t\tif (toCollectionFailureWarningKind(failure) !== 'auth') continue;\n\t\tif (toProviderWarningScope(failure.scope) == null || (failure as ProviderScopeFailure).credentialRefused) {\n\t\t\treturn false;\n\t\t}\n\n\t\tscoped = true;\n\t}\n\treturn scoped;\n}\n\n/** The scoped authentication failures in `metadata` that kept their refusal, with the scope they are forwarded as. */\nfunction* refusedScopes(\n\tmetadata: CollectionMetadata | undefined,\n): Generator<{ failure: ProviderScopeFailure & { refusal: ProviderRefusal }; scope: ProviderWarningScope }> {\n\tfor (const failure of (metadata?.failures ?? []) as ProviderScopeFailure[]) {\n\t\tif (\n\t\t\tfailure.refusal == null ||\n\t\t\tfailure.credentialRefused ||\n\t\t\ttoCollectionFailureWarningKind(failure) !== 'auth'\n\t\t) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst scope = toProviderWarningScope(failure.scope);\n\t\tif (scope == null) continue;\n\n\t\tyield { failure: failure as ProviderScopeFailure & { refusal: ProviderRefusal }, scope: scope };\n\t}\n}\n\n/**\n * Marks each scoped authentication failure in `metadata` whose refusal the provider pins on the credential, so it is\n * published for the connection rather than for its scope. Returns whether any was.\n */\nexport function markCredentialRefusals(\n\tmetadata: CollectionMetadata | undefined,\n\tisCredentialRefusal: (refusal: ProviderRefusal) => boolean,\n): boolean {\n\tlet marked = false;\n\tfor (const { failure } of refusedScopes(metadata)) {\n\t\tif (!isCredentialRefusal(failure.refusal)) continue;\n\n\t\tfailure.credentialRefused = true;\n\t\tmarked = true;\n\t}\n\treturn marked;\n}\n\n/**\n * Names the cause of each scoped authentication failure in `metadata` that kept its refusal. Only for a read whose\n * credential was just confirmed: before that, the refusals this can name look exactly like a dead credential.\n */\nexport function attributeScopedAuthFailures(\n\tmetadata: CollectionMetadata | undefined,\n\tdescribe: (refusal: ProviderRefusal, scope: ProviderWarningScope) => ProviderWarningCause | undefined,\n): void {\n\tfor (const { failure, scope } of refusedScopes(metadata)) {\n\t\tif (failure.cause != null) continue;\n\n\t\tconst cause = describe(failure.refusal, scope);\n\t\tif (cause != null) {\n\t\t\tfailure.cause = cause;\n\t\t}\n\t}\n}\n\n/**\n * Explains one omission in the consumer's terms — what was left out and, where the SDK reports it, how much.\n *\n * An omission is a completeness fact, never a failure: the read succeeded, and the provider (or the SDK's own\n * recovery budget) is what withheld results. That is why these never contribute to `fetchFailed`. Whether a\n * retry would recover anything is a separate question this layer cannot answer — see the `recovery` note on\n * {@link toProviderWarningOmission}.\n */\nfunction collectionOmissionMessage(omission: CollectionOmission): string {\n\tconst scopeText = collectionScopeText(omission.scope);\n\tswitch (omission.kind) {\n\t\tcase 'provider-limit':\n\t\t\t// `totalCount` is the only figure saying how much was withheld; both fields are optional, so degrade\n\t\t\t// through the shapes the SDK can actually emit rather than printing `undefined`.\n\t\t\tif (omission.totalCount != null && omission.limit != null) {\n\t\t\t\treturn `Search${scopeText} matched ${omission.totalCount} results, but the provider exposes at most ${omission.limit}`;\n\t\t\t}\n\t\t\tif (omission.limit != null) {\n\t\t\t\treturn `Search${scopeText} exceeded the provider limit of ${omission.limit} results`;\n\t\t\t}\n\t\t\treturn `Search${scopeText} exceeded the provider's result limit`;\n\t\tcase 'recovery-budget':\n\t\t\treturn `Stopped recovering omitted results${scopeText} after reaching the request budget${\n\t\t\t\tomission.limit != null ? ` of ${omission.limit} requests` : ''\n\t\t\t}`;\n\t\tcase 'pagination-incomplete':\n\t\t\treturn `More results are available${scopeText} than this read returned`;\n\t\tdefault:\n\t\t\t// `CollectionOmissionKind` is a closed union, so this is unreachable today. Kept as a compile-time\n\t\t\t// guard rather than a silent fallback: an SDK bump that adds a kind breaks the build here instead\n\t\t\t// of quietly degrading it to a vague message.\n\t\t\tomission.kind satisfies never;\n\t\t\treturn `Some results were omitted${scopeText}; the read is incomplete`;\n\t}\n}\n\n/**\n * Forwards an SDK omission as the structured signal consumers read instead of parsing {@link\n * collectionOmissionMessage}'s prose.\n *\n * `results.ts` re-spells `CollectionOmissionKind` rather than importing it, so the published warning surface\n * carries no `@gitkraken/provider-apis` types (see the export block in `index.ts`). The return type is what\n * keeps the two unions honest: an SDK bump that adds a member fails to compile here, alongside\n * `collectionOmissionMessage`'s `satisfies never`.\n *\n * `scope` is copied because the SDK's own object is retained and re-merged across drained pages\n * (`providerPaging.ts`), and this one crosses the package boundary.\n *\n * `recovery` is always `'none'`, deliberately, and a scoped `pagination-incomplete` is why it looks wrong: the\n * SDK emits that one shape from situations with opposite remedies and says so itself — \"Either the read\n * deliberately took one page per scope, or the provider advertised another page it gave no way to reach.\" Its\n * `collectAcrossScopes` producers (Azure, Bitbucket, Bitbucket Server) are the first, its `drainAcrossScopes`\n * producers (GitLab, Jira) the second, and those report an omission only when a cursor STALLED — so re-reading\n * that scope stalls at the identical page. Nothing in `CollectionOmission` separates the two, so claiming\n * recoverability would ship the dead-end button `recovery` exists to prevent. Making the recoverable case\n * claimable needs provider-apis to say whether a scope was drained or merely sampled: a fact to forward, not\n * to guess here.\n */\nfunction toProviderWarningOmission(omission: CollectionOmission): ProviderWarningOmission {\n\treturn {\n\t\tkind: omission.kind,\n\t\trecovery: 'none',\n\t\t...(omission.limit != null ? { limit: omission.limit } : {}),\n\t\t// The SDK types this `number | null | undefined`; normalize to one absence for consumers.\n\t\t...(omission.totalCount != null ? { totalCount: omission.totalCount } : {}),\n\t\t...(omission.scope != null ? { scope: { ...omission.scope } } : {}),\n\t};\n}\n\n/**\n * Whether SDK metadata describes a read that may be missing results.\n *\n * A reported omission counts on its own rather than deferring to `completeness`. The SDK does couple the\n * two — every site that emits an omission degrades completeness in the same call — so today the omission\n * clause changes no outcome. It is defensive: the two facts arrive as independent fields, and a read that\n * names what it left out must not be publishable as whole if a future producer sets only one of them.\n *\n * This is the single definition of \"may be missing results\", shared with `getPagedResult`, so one metadata\n * object cannot be truncated at one layer and complete at another.\n */\nexport function isIncompleteCollection(metadata: CollectionMetadata | undefined): boolean {\n\tif (metadata == null) return false;\n\n\treturn metadata.completeness !== 'complete' || (metadata.omissions?.length ?? 0) > 0;\n}\n\n/** Converts internal SDK collection metadata into neutral provider facade signals. */\nexport function assessCollectionMetadata(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tmetadata: CollectionMetadata | undefined,\n): { warnings: ProviderWarning[]; fetchFailed: boolean; truncated: boolean } {\n\tif (metadata == null) return { warnings: [], fetchFailed: false, truncated: false };\n\n\tconst warnings: ProviderWarning[] = [];\n\tconst failures: ProviderScopeFailure[] = metadata.failures ?? [];\n\tfor (const failure of failures) {\n\t\tconst kind = toCollectionFailureWarningKind(failure);\n\t\tconst scope = failure.credentialRefused ? undefined : toProviderWarningScope(failure.scope);\n\t\tappendDedupedWarning(warnings, {\n\t\t\tproviderId: providerId,\n\t\t\tdomain: domain,\n\t\t\tconnectionId: connectionId,\n\t\t\t// A credential-level refusal names no scope in prose either, so every scope's copy of it is one warning.\n\t\t\tmessage: collectionFailureMessage(failure.credentialRefused ? { ...failure, scope: undefined } : failure),\n\t\t\tkind: kind,\n\t\t\tisAuth: kind === 'auth',\n\t\t\t...(scope != null ? { scope: scope } : {}),\n\t\t\t// Copied for the reason the scope is: the failure object is retained and re-merged across pages.\n\t\t\t...(failure.cause != null ? { cause: { ...failure.cause } } : {}),\n\t\t});\n\t}\n\n\t// Omissions explain WHY a read is incomplete when nothing failed — a provider cap, an exhausted recovery\n\t// budget, an undrained scope. They classify as `other`, never `auth`, and are deliberately excluded from\n\t// `fetchFailed` below: the request itself succeeded. `omission` carries that same fact structurally, so a\n\t// consumer can act on it without parsing the message.\n\tconst omissions = metadata.omissions ?? [];\n\tfor (const omission of omissions) {\n\t\tappendDedupedWarning(warnings, {\n\t\t\tproviderId: providerId,\n\t\t\tdomain: domain,\n\t\t\tconnectionId: connectionId,\n\t\t\tmessage: collectionOmissionMessage(omission),\n\t\t\tkind: 'other',\n\t\t\tisAuth: false,\n\t\t\tomission: toProviderWarningOmission(omission),\n\t\t});\n\t}\n\n\tconst incomplete = isIncompleteCollection(metadata);\n\t// Only fall back to the generic message when nothing more specific was reported; an omission already\n\t// explains the incompleteness in the consumer's terms, so adding this on top would be noise.\n\t//\n\t// This one deliberately carries no `omission`: it fires precisely when the SDK reported incompleteness\n\t// WITHOUT saying what was left out, so there is no structured fact to forward and synthesizing one would\n\t// assert a specificity this layer does not have.\n\tif (incomplete && failures.length === 0 && omissions.length === 0) {\n\t\tappendDedupedWarning(warnings, {\n\t\t\tproviderId: providerId,\n\t\t\tdomain: domain,\n\t\t\tconnectionId: connectionId,\n\t\t\tmessage:\n\t\t\t\tmetadata.completeness === 'partial'\n\t\t\t\t\t? 'Some results were omitted; the read is incomplete'\n\t\t\t\t\t: 'Result completeness could not be confirmed',\n\t\t\tkind: 'other',\n\t\t\tisAuth: false,\n\t\t});\n\t}\n\n\treturn { warnings: warnings, fetchFailed: failures.length > 0, truncated: incomplete };\n}\n\n/**\n * Appends neutral warnings derived from SDK metadata and returns its failure/truncation assessment.\n *\n * `reported` says whether the metadata described this read's incompleteness at all. A caller that would\n * otherwise add its own generic \"this was truncated\" warning uses it to stay quiet — the same rule\n * {@link assessCollectionMetadata} applies to its own generic fallback, one layer up: whatever the metadata\n * reported already names this provider and, unless it was that same fallback, says something a consumer can\n * act on. Restating it in a weaker vocabulary is how one gap becomes two warnings with two different verdicts.\n *\n * `truncated` is the near-synonym that must NOT be used for that question: it comes from\n * {@link isIncompleteCollection}, which does not consider failures, so metadata carrying a typed failure\n * alongside `completeness: 'complete'` reports `truncated: false` while having already appended the warning\n * that explains everything. Gating on it would restate exactly the case this field exists to keep quiet.\n */\nexport function mergeAssessmentInto(\n\twarnings: ProviderWarning[],\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tmetadata: CollectionMetadata | undefined,\n): { fetchFailed: boolean; truncated: boolean; reported: boolean } {\n\tconst assessment = assessCollectionMetadata(providerId, domain, connectionId, metadata);\n\tfor (const warning of assessment.warnings) {\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\treturn {\n\t\tfetchFailed: assessment.fetchFailed,\n\t\ttruncated: assessment.truncated,\n\t\t// What the assessment PRODUCED, not what `appendDedupedWarning` accepted: a warning dropped as a\n\t\t// duplicate is still one the consumer will see, so it still makes a restatement redundant.\n\t\treported: assessment.warnings.length > 0,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"collectionMetadata.js","names":[],"sources":["../../../../plus/integrations/src/collectionMetadata.ts"],"sourcesContent":["import type {\n\tCollectionMetadata,\n\tCollectionOmission,\n\tCollectionScope,\n\tCollectionScopeFailure,\n} from '@gitkraken/provider-apis';\n// Matched by its `code` discriminator rather than by `instanceof`: the SDK ships one bundle per entry point, so\n// the class reached through the root is not the same object as the one reached through `/providers`.\nimport { isInvalidRequestError, isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport {\n\tAuthenticationError,\n\tRequestClientError,\n\tRequestNotFoundError,\n\tRequestRateLimitError,\n} from '@gitlens/git/errors.js';\nimport type { TokenWithInfo } from './authentication/models.js';\nimport type { IntegrationIds } from './constants.js';\nimport type { ResponseHeaders } from './errors.js';\nimport { getResponseHeader, isProviderUnreachableError, isRateLimitResponse } from './errors.js';\nimport { getProviderResponseBodyMessage } from './providers/providerErrors.js';\nimport type {\n\tProviderWarning,\n\tProviderWarningCause,\n\tProviderWarningOmission,\n\tProviderWarningScope,\n} from './results.js';\nimport { appendDedupedWarning, providerWarningMessage } from './results.js';\n\n/**\n * Re-throws an error that is a fact about the CALL rather than about one scope of a fan-out.\n *\n * Every provider fan-out catches per-scope rejections and records them as {@link CollectionScopeFailure}s, which is\n * right for auth, rate limits and a missing project: those really did happen to one scope, and the siblings that\n * succeeded must survive. A caller-contract error is the opposite — it is identical for every scope and was decided\n * before any request went out — so degrading it would hand back N indistinguishable failures and an empty `partial`\n * page, describing an invalid call as an incomplete read.\n *\n * Today that means the SDK's `UnsupportedSortError` and `InvalidRequestError` — the two codes it lists in its own\n * `CALLER_CONTRACT_ERROR_CODES`, both documented as \"never degraded into a `CollectionScopeFailure` or an\n * omission\". The second covers arguments rejected before the first request goes out: a project key list longer\n * than one Jira search can carry (or an empty one), a non-integer `pageSize`, an unknown current-user filter, or a\n * Jira Server read with no `baseUrl` to address. Called from every fan-out rather than spelled out in each, so a\n * fan-out added later inherits the rule instead of silently omitting it — which is exactly how Azure's\n * account-wide drain came to degrade it while its three siblings did not.\n */\nexport function throwIfCallerContractError(ex: unknown): void {\n\tif (isUnsupportedSortError(ex) || isInvalidRequestError(ex)) throw ex;\n}\n\n/**\n * Whether a failed query that covered several scopes would fail each scope's own read the same way: a rejected or\n * rate-limited token, or a call the SDK refused before sending. Anything else (a query the provider rejects because\n * one scope is gone or no longer visible, a server error) may be one scope's problem, so the scopes are worth\n * reading on their own.\n */\nexport function failsEveryScope(ex: unknown): boolean {\n\tconst kind = toCollectionFailureKind(ex);\n\treturn (\n\t\tkind === 'authentication' || kind === 'rate-limit' || isUnsupportedSortError(ex) || isInvalidRequestError(ex)\n\t);\n}\n\n/**\n * Maps a caught GitLens request error to the SDK collection failure vocabulary used inside provider fan-outs.\n */\nexport function toCollectionFailureKind(ex: unknown): CollectionScopeFailure['kind'] {\n\tif (ex instanceof AuthenticationError) return 'authentication';\n\tif (ex instanceof RequestRateLimitError) return 'rate-limit';\n\tif (ex instanceof RequestNotFoundError) return 'not-found';\n\treturn 'provider';\n}\n\n/**\n * What an authentication refusal's response said, kept on the failure so the provider can name its cause once the\n * credential behind it is confirmed (see `IntegrationBase.confirmScopedAuthFailures`). Only these facts are kept,\n * never the response: its headers carry the provider's session cookies.\n */\nexport interface ProviderRefusal {\n\tstatus: number;\n\t/** The provider's own explanation: Azure DevOps' `X-TFS-ServiceError`, or the error body's message. */\n\tdetail?: string;\n\t/** The provider's error type, when the body names one (Azure DevOps `typeKey`). */\n\ttypeKey?: string;\n\t/**\n\t * The OAuth app the refused token belongs to, when the provider says (GitHub's `X-OAuth-Client-Id`). Not a\n\t * secret: it is the app's public client id, which the page where a user asks an organization to approve the app\n\t * is addressed by.\n\t */\n\toauthClientId?: string;\n}\n\n/** A scope failure as recorded here: the SDK's shape, plus what the refusal said and, once it is known, why. */\nexport interface ProviderScopeFailure extends CollectionScopeFailure {\n\trefusal?: ProviderRefusal;\n\tcause?: ProviderWarningCause;\n\t/** The provider pinned the refusal on the credential itself, so it is published for the connection, unscoped. */\n\tcredentialRefused?: true;\n}\n\n/**\n * Reads the refusal off an `AuthenticationError` that carries the provider's response: the SDK adapter's plain\n * `{ status, headers, body }` (`providersApi.ts`), Octokit's `{ status, headers, data }` (the GitHub client's REST\n * reads), or the `Response` of a `ProviderFetchError`, whose body was already read into its message.\n */\nfunction toProviderRefusal(ex: unknown): ProviderRefusal | undefined {\n\tif (!(ex instanceof AuthenticationError)) return undefined;\n\n\tconst response = (ex.original as { response?: unknown } | undefined)?.response as\n\t\t| { status?: unknown; headers?: ResponseHeaders; body?: unknown; data?: unknown }\n\t\t| undefined;\n\tif (typeof response?.status !== 'number') return undefined;\n\n\t// Only a structured body: a string one is a page (a proxy's, a sign-in form), not an explanation.\n\tconst raw = response.body ?? response.data;\n\tconst body = typeof raw === 'object' && raw != null ? raw : undefined;\n\tconst detail =\n\t\tdecodeServiceError(getResponseHeader(response.headers, 'x-tfs-serviceerror')) ??\n\t\tgetProviderResponseBodyMessage(body);\n\tconst typeKey = body != null && 'typeKey' in body ? body.typeKey : undefined;\n\tconst oauthClientId = getResponseHeader(response.headers, 'x-oauth-client-id');\n\n\treturn {\n\t\tstatus: response.status,\n\t\t...(detail ? { detail: detail } : {}),\n\t\t...(typeof typeKey === 'string' ? { typeKey: typeKey } : {}),\n\t\t...(typeof oauthClientId === 'string' && oauthClientId ? { oauthClientId: oauthClientId } : {}),\n\t};\n}\n\n/** Azure DevOps sends `X-TFS-ServiceError` percent-encoded. */\nfunction decodeServiceError(value: unknown): string | undefined {\n\tif (typeof value !== 'string' || !value) return undefined;\n\n\ttry {\n\t\treturn decodeURIComponent(value).trim() || undefined;\n\t} catch {\n\t\treturn value.trim() || undefined;\n\t}\n}\n\n/**\n * Builds a structured SDK scope failure from a caught GitLens request error.\n *\n * An authentication refusal is described in the provider's own words when it gives any, and otherwise by the\n * error the provider's response raised, sanitized like any warning's (a page becomes its status), rather than by\n * `AuthenticationError`'s generic \"credentials are either invalid or expired\": a scope refusing a sound credential\n * is the case this failure exists to tell apart, and that sentence says the opposite.\n */\nexport function toCollectionScopeFailure(scope: CollectionScopeFailure['scope'], ex: unknown): ProviderScopeFailure {\n\tconst kind = toCollectionFailureKind(ex);\n\tconst refusal = toProviderRefusal(ex);\n\tconst message =\n\t\trefusal != null ? (refusal.detail ?? providerWarningMessage((ex as AuthenticationError).original)) : undefined;\n\treturn {\n\t\tscope: scope,\n\t\tkind: kind,\n\t\t...(message ? { message: message } : ex instanceof Error && ex.message ? { message: ex.message } : {}),\n\t\t...(refusal != null ? { refusal: refusal } : {}),\n\t\t...(kind === 'provider' && isProviderUnreachableError(ex) ? { cause: { reason: 'unreachable' as const } } : {}),\n\t};\n}\n\nfunction collectionFailureKindToWarningKind(kind: CollectionScopeFailure['kind']): ProviderWarning['kind'] {\n\tswitch (kind) {\n\t\tcase 'authentication':\n\t\t\treturn 'auth';\n\t\tcase 'rate-limit':\n\t\t\treturn 'rate-limit';\n\t\tcase 'not-found':\n\t\t\treturn 'not-found';\n\t\tdefault:\n\t\t\treturn 'other';\n\t}\n}\n\nfunction getCollectionFailureStatus(message: string | undefined): number | undefined {\n\tif (message == null) return undefined;\n\n\tconst match = /\\((\\d{3})\\)/.exec(message) ?? /^(?:HTTP\\s+)?(\\d{3})\\b/i.exec(message.trim());\n\treturn match != null ? Number(match[1]) : undefined;\n}\n\nfunction toCollectionFailureWarningKind(failure: CollectionScopeFailure): ProviderWarning['kind'] {\n\tconst mapped = collectionFailureKindToWarningKind(failure.kind);\n\tif (mapped !== 'other' || (failure.kind !== 'provider' && failure.kind !== 'unknown')) return mapped;\n\n\t// provider-apis currently groups several HTTP statuses under `provider` in partial fan-outs. The custom\n\t// fetch adapter retains the status in `message`, so recover the same neutral classification the thrown-error\n\t// path exposes instead of downgrading these structured failures to `other`.\n\tconst status = getCollectionFailureStatus(failure.message);\n\tswitch (status) {\n\t\tcase 401:\n\t\t\treturn 'auth';\n\t\tcase 403:\n\t\t\treturn isRateLimitResponse({ status: status, message: failure.message }) ? 'rate-limit' : 'auth';\n\t\tcase 404:\n\t\tcase 410:\n\t\tcase 422:\n\t\t\treturn 'not-found';\n\t\tcase 429:\n\t\t\treturn 'rate-limit';\n\t\tdefault:\n\t\t\treturn mapped;\n\t}\n}\n\nfunction toCollectionFailureCause(\n\tfailure: ProviderScopeFailure,\n\tkind: ProviderWarning['kind'],\n): ProviderWarningCause | undefined {\n\treturn (\n\t\tfailure.cause ??\n\t\t(kind === 'other' &&\n\t\tisProviderUnreachableError({ ...failure, status: getCollectionFailureStatus(failure.message) })\n\t\t\t? { reason: 'unreachable' }\n\t\t\t: undefined)\n\t);\n}\n\nexport function toCollectionFailureError(failure: ProviderScopeFailure, tokenWithInfo: TokenWithInfo): Error {\n\tconst kind = toCollectionFailureWarningKind(failure);\n\tconst error = new Error(failure.message ?? 'Provider request failed', {\n\t\tcause: toCollectionFailureCause(failure, kind),\n\t});\n\tconst { accessToken, ...tokenInfo } = tokenWithInfo;\n\tif (kind === 'auth') return new AuthenticationError(tokenInfo, error.message, error);\n\tif (kind === 'rate-limit') return new RequestRateLimitError(error, accessToken, undefined);\n\tif (kind === 'not-found') return new RequestNotFoundError(error);\n\n\tconst status = getCollectionFailureStatus(failure.message);\n\tif (kind === 'other' && status != null && status >= 400 && status < 500) {\n\t\treturn new RequestClientError(error);\n\t}\n\n\treturn error;\n}\n\n/** ` (resource r, project p, repository o/n)` for the scope IDs present; empty when the scope names none. */\nfunction collectionScopeText(scope: CollectionScope | undefined): string {\n\tconst parts: string[] = [];\n\tif (scope?.resourceId != null) {\n\t\tparts.push(`resource ${scope.resourceId}`);\n\t}\n\tif (scope?.projectId != null) {\n\t\tparts.push(`project ${scope.projectId}`);\n\t}\n\tif (scope?.repositoryId != null) {\n\t\tparts.push(`repository ${scope.repositoryId}`);\n\t}\n\n\treturn parts.length ? ` (${parts.join(', ')})` : '';\n}\n\n/** The cause in the consumer's terms, for the prose that goes with the structured {@link ProviderWarningCause}. */\nfunction causeText(cause: ProviderWarningCause): string {\n\tswitch (cause.reason) {\n\t\tcase 'oauth-app-not-allowed':\n\t\t\treturn 'the organization does not allow third-party OAuth apps';\n\t\tcase 'access-denied':\n\t\t\treturn 'the account has no access to it';\n\t\tcase 'conditional-access':\n\t\t\treturn 'a Conditional Access policy blocked the request';\n\t\tcase 'unreachable':\n\t\t\treturn 'the provider server could not be reached';\n\t\tdefault:\n\t\t\tcause.reason satisfies never;\n\t\t\treturn 'the provider refused the request';\n\t}\n}\n\nfunction collectionFailureMessage(failure: ProviderScopeFailure): string {\n\tconst text = failure.cause != null ? causeText(failure.cause) : failure.message;\n\tconst detail = text != null ? `: ${text}` : '';\n\treturn `Failed to read ${failure.kind} scope${collectionScopeText(failure.scope)}${detail}`;\n}\n\n/**\n * Forwards the part of a failure's scope below the provider, so a consumer can tell \"one organization refused\n * this token\" from \"the connection's token is dead\" without parsing {@link collectionFailureMessage}.\n *\n * `providerId` is dropped: the warning already names its provider, and a scope naming nothing below it is a\n * failure of the connection itself. That is the distinction `getOrganizationsForUserResult` also draws before\n * counting an auth failure against the connection, though it reads a scope with no keys but `providerId`, so an\n * absent scope does not count there. Forwarding such a scope would make an account-wide failure look confined.\n *\n * Copied field by field, for the reason {@link toProviderWarningOmission} copies its scope.\n */\nfunction toProviderWarningScope(scope: CollectionScope | undefined): ProviderWarningScope | undefined {\n\tif (scope == null) return undefined;\n\n\tconst { resourceId, projectId, repositoryId } = scope;\n\tif (resourceId == null && projectId == null && repositoryId == null) return undefined;\n\n\treturn {\n\t\t...(resourceId != null ? { resourceId: resourceId } : {}),\n\t\t...(projectId != null ? { projectId: projectId } : {}),\n\t\t...(repositoryId != null ? { repositoryId: repositoryId } : {}),\n\t};\n}\n\n/**\n * Whether `metadata` reports authentication failures only for scopes below the provider, with none for the\n * connection itself. A credential revoked since a provider cached its discovery produces exactly this shape,\n * the same one a scope's own refusal does; `IntegrationBase.confirmScopedAuthFailures` tells them apart.\n *\n * Classified exactly as {@link assessCollectionMetadata} classifies the warnings, so it agrees with what a\n * consumer would be shown.\n */\nexport function hasOnlyScopedAuthFailures(metadata: CollectionMetadata | undefined): boolean {\n\tlet scoped = false;\n\tfor (const failure of metadata?.failures ?? []) {\n\t\tif (toCollectionFailureWarningKind(failure) !== 'auth') continue;\n\t\tif (toProviderWarningScope(failure.scope) == null || (failure as ProviderScopeFailure).credentialRefused) {\n\t\t\treturn false;\n\t\t}\n\n\t\tscoped = true;\n\t}\n\treturn scoped;\n}\n\n/** The scoped authentication failures in `metadata` that kept their refusal, with the scope they are forwarded as. */\nfunction* refusedScopes(\n\tmetadata: CollectionMetadata | undefined,\n): Generator<{ failure: ProviderScopeFailure & { refusal: ProviderRefusal }; scope: ProviderWarningScope }> {\n\tfor (const failure of (metadata?.failures ?? []) as ProviderScopeFailure[]) {\n\t\tif (\n\t\t\tfailure.refusal == null ||\n\t\t\tfailure.credentialRefused ||\n\t\t\ttoCollectionFailureWarningKind(failure) !== 'auth'\n\t\t) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst scope = toProviderWarningScope(failure.scope);\n\t\tif (scope == null) continue;\n\n\t\tyield { failure: failure as ProviderScopeFailure & { refusal: ProviderRefusal }, scope: scope };\n\t}\n}\n\n/**\n * Marks each scoped authentication failure in `metadata` whose refusal the provider pins on the credential, so it is\n * published for the connection rather than for its scope. Returns whether any was.\n */\nexport function markCredentialRefusals(\n\tmetadata: CollectionMetadata | undefined,\n\tisCredentialRefusal: (refusal: ProviderRefusal) => boolean,\n): boolean {\n\tlet marked = false;\n\tfor (const { failure } of refusedScopes(metadata)) {\n\t\tif (!isCredentialRefusal(failure.refusal)) continue;\n\n\t\tfailure.credentialRefused = true;\n\t\tmarked = true;\n\t}\n\treturn marked;\n}\n\n/**\n * Names the cause of each scoped authentication failure in `metadata` that kept its refusal. Only for a read whose\n * credential was just confirmed: before that, the refusals this can name look exactly like a dead credential.\n */\nexport function attributeScopedAuthFailures(\n\tmetadata: CollectionMetadata | undefined,\n\tdescribe: (refusal: ProviderRefusal, scope: ProviderWarningScope) => ProviderWarningCause | undefined,\n): void {\n\tfor (const { failure, scope } of refusedScopes(metadata)) {\n\t\tif (failure.cause != null) continue;\n\n\t\tconst cause = describe(failure.refusal, scope);\n\t\tif (cause != null) {\n\t\t\tfailure.cause = cause;\n\t\t}\n\t}\n}\n\n/**\n * Explains one omission in the consumer's terms — what was left out and, where the SDK reports it, how much.\n *\n * An omission is a completeness fact, never a failure: the read succeeded, and the provider (or the SDK's own\n * recovery budget) is what withheld results. That is why these never contribute to `fetchFailed`. Whether a\n * retry would recover anything is a separate question this layer cannot answer — see the `recovery` note on\n * {@link toProviderWarningOmission}.\n */\nfunction collectionOmissionMessage(omission: CollectionOmission): string {\n\tconst scopeText = collectionScopeText(omission.scope);\n\tswitch (omission.kind) {\n\t\tcase 'provider-limit':\n\t\t\t// `totalCount` is the only figure saying how much was withheld; both fields are optional, so degrade\n\t\t\t// through the shapes the SDK can actually emit rather than printing `undefined`.\n\t\t\tif (omission.totalCount != null && omission.limit != null) {\n\t\t\t\treturn `Search${scopeText} matched ${omission.totalCount} results, but the provider exposes at most ${omission.limit}`;\n\t\t\t}\n\t\t\tif (omission.limit != null) {\n\t\t\t\treturn `Search${scopeText} exceeded the provider limit of ${omission.limit} results`;\n\t\t\t}\n\t\t\treturn `Search${scopeText} exceeded the provider's result limit`;\n\t\tcase 'recovery-budget':\n\t\t\treturn `Stopped recovering omitted results${scopeText} after reaching the request budget${\n\t\t\t\tomission.limit != null ? ` of ${omission.limit} requests` : ''\n\t\t\t}`;\n\t\tcase 'pagination-incomplete':\n\t\t\treturn `More results are available${scopeText} than this read returned`;\n\t\tdefault:\n\t\t\t// `CollectionOmissionKind` is a closed union, so this is unreachable today. Kept as a compile-time\n\t\t\t// guard rather than a silent fallback: an SDK bump that adds a kind breaks the build here instead\n\t\t\t// of quietly degrading it to a vague message.\n\t\t\tomission.kind satisfies never;\n\t\t\treturn `Some results were omitted${scopeText}; the read is incomplete`;\n\t}\n}\n\n/**\n * Forwards an SDK omission as the structured signal consumers read instead of parsing {@link\n * collectionOmissionMessage}'s prose.\n *\n * `results.ts` re-spells `CollectionOmissionKind` rather than importing it, so the published warning surface\n * carries no `@gitkraken/provider-apis` types (see the export block in `index.ts`). The return type is what\n * keeps the two unions honest: an SDK bump that adds a member fails to compile here, alongside\n * `collectionOmissionMessage`'s `satisfies never`.\n *\n * `scope` is copied because the SDK's own object is retained and re-merged across drained pages\n * (`providerPaging.ts`), and this one crosses the package boundary.\n *\n * `recovery` is always `'none'`, deliberately, and a scoped `pagination-incomplete` is why it looks wrong: the\n * SDK emits that one shape from situations with opposite remedies and says so itself — \"Either the read\n * deliberately took one page per scope, or the provider advertised another page it gave no way to reach.\" Its\n * `collectAcrossScopes` producers (Azure, Bitbucket, Bitbucket Server) are the first, its `drainAcrossScopes`\n * producers (GitLab, Jira) the second, and those report an omission only when a cursor STALLED — so re-reading\n * that scope stalls at the identical page. Nothing in `CollectionOmission` separates the two, so claiming\n * recoverability would ship the dead-end button `recovery` exists to prevent. Making the recoverable case\n * claimable needs provider-apis to say whether a scope was drained or merely sampled: a fact to forward, not\n * to guess here.\n */\nfunction toProviderWarningOmission(omission: CollectionOmission): ProviderWarningOmission {\n\treturn {\n\t\tkind: omission.kind,\n\t\trecovery: 'none',\n\t\t...(omission.limit != null ? { limit: omission.limit } : {}),\n\t\t// The SDK types this `number | null | undefined`; normalize to one absence for consumers.\n\t\t...(omission.totalCount != null ? { totalCount: omission.totalCount } : {}),\n\t\t...(omission.scope != null ? { scope: { ...omission.scope } } : {}),\n\t};\n}\n\n/**\n * Whether SDK metadata describes a read that may be missing results.\n *\n * A reported omission counts on its own rather than deferring to `completeness`. The SDK does couple the\n * two — every site that emits an omission degrades completeness in the same call — so today the omission\n * clause changes no outcome. It is defensive: the two facts arrive as independent fields, and a read that\n * names what it left out must not be publishable as whole if a future producer sets only one of them.\n *\n * This is the single definition of \"may be missing results\", shared with `getPagedResult`, so one metadata\n * object cannot be truncated at one layer and complete at another.\n */\nexport function isIncompleteCollection(metadata: CollectionMetadata | undefined): boolean {\n\tif (metadata == null) return false;\n\n\treturn metadata.completeness !== 'complete' || (metadata.omissions?.length ?? 0) > 0;\n}\n\n/** Converts internal SDK collection metadata into neutral provider facade signals. */\nexport function assessCollectionMetadata(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tmetadata: CollectionMetadata | undefined,\n): { warnings: ProviderWarning[]; fetchFailed: boolean; truncated: boolean } {\n\tif (metadata == null) return { warnings: [], fetchFailed: false, truncated: false };\n\n\tconst warnings: ProviderWarning[] = [];\n\tconst failures: ProviderScopeFailure[] = metadata.failures ?? [];\n\tfor (const failure of failures) {\n\t\tconst kind = toCollectionFailureWarningKind(failure);\n\t\tconst cause = toCollectionFailureCause(failure, kind);\n\t\tconst scope = failure.credentialRefused ? undefined : toProviderWarningScope(failure.scope);\n\t\tappendDedupedWarning(warnings, {\n\t\t\tproviderId: providerId,\n\t\t\tdomain: domain,\n\t\t\tconnectionId: connectionId,\n\t\t\t// A credential-level refusal names no scope in prose either, so every scope's copy of it is one warning.\n\t\t\tmessage: collectionFailureMessage({\n\t\t\t\t...failure,\n\t\t\t\tcause: cause,\n\t\t\t\t...(failure.credentialRefused ? { scope: undefined } : {}),\n\t\t\t}),\n\t\t\tkind: kind,\n\t\t\tisAuth: kind === 'auth',\n\t\t\t...(scope != null ? { scope: scope } : {}),\n\t\t\t// Copied for the reason the scope is: the failure object is retained and re-merged across pages.\n\t\t\t...(cause != null ? { cause: { ...cause } } : {}),\n\t\t});\n\t}\n\n\t// Omissions explain WHY a read is incomplete when nothing failed — a provider cap, an exhausted recovery\n\t// budget, an undrained scope. They classify as `other`, never `auth`, and are deliberately excluded from\n\t// `fetchFailed` below: the request itself succeeded. `omission` carries that same fact structurally, so a\n\t// consumer can act on it without parsing the message.\n\tconst omissions = metadata.omissions ?? [];\n\tfor (const omission of omissions) {\n\t\tappendDedupedWarning(warnings, {\n\t\t\tproviderId: providerId,\n\t\t\tdomain: domain,\n\t\t\tconnectionId: connectionId,\n\t\t\tmessage: collectionOmissionMessage(omission),\n\t\t\tkind: 'other',\n\t\t\tisAuth: false,\n\t\t\tomission: toProviderWarningOmission(omission),\n\t\t});\n\t}\n\n\tconst incomplete = isIncompleteCollection(metadata);\n\t// Only fall back to the generic message when nothing more specific was reported; an omission already\n\t// explains the incompleteness in the consumer's terms, so adding this on top would be noise.\n\t//\n\t// This one deliberately carries no `omission`: it fires precisely when the SDK reported incompleteness\n\t// WITHOUT saying what was left out, so there is no structured fact to forward and synthesizing one would\n\t// assert a specificity this layer does not have.\n\tif (incomplete && failures.length === 0 && omissions.length === 0) {\n\t\tappendDedupedWarning(warnings, {\n\t\t\tproviderId: providerId,\n\t\t\tdomain: domain,\n\t\t\tconnectionId: connectionId,\n\t\t\tmessage:\n\t\t\t\tmetadata.completeness === 'partial'\n\t\t\t\t\t? 'Some results were omitted; the read is incomplete'\n\t\t\t\t\t: 'Result completeness could not be confirmed',\n\t\t\tkind: 'other',\n\t\t\tisAuth: false,\n\t\t});\n\t}\n\n\treturn { warnings: warnings, fetchFailed: failures.length > 0, truncated: incomplete };\n}\n\n/**\n * Appends neutral warnings derived from SDK metadata and returns its failure/truncation assessment.\n *\n * `reported` says whether the metadata described this read's incompleteness at all. A caller that would\n * otherwise add its own generic \"this was truncated\" warning uses it to stay quiet — the same rule\n * {@link assessCollectionMetadata} applies to its own generic fallback, one layer up: whatever the metadata\n * reported already names this provider and, unless it was that same fallback, says something a consumer can\n * act on. Restating it in a weaker vocabulary is how one gap becomes two warnings with two different verdicts.\n *\n * `truncated` is the near-synonym that must NOT be used for that question: it comes from\n * {@link isIncompleteCollection}, which does not consider failures, so metadata carrying a typed failure\n * alongside `completeness: 'complete'` reports `truncated: false` while having already appended the warning\n * that explains everything. Gating on it would restate exactly the case this field exists to keep quiet.\n */\nexport function mergeAssessmentInto(\n\twarnings: ProviderWarning[],\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tmetadata: CollectionMetadata | undefined,\n): { fetchFailed: boolean; truncated: boolean; reported: boolean } {\n\tconst assessment = assessCollectionMetadata(providerId, domain, connectionId, metadata);\n\tfor (const warning of assessment.warnings) {\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\treturn {\n\t\tfetchFailed: assessment.fetchFailed,\n\t\ttruncated: assessment.truncated,\n\t\t// What the assessment PRODUCED, not what `appendDedupedWarning` accepted: a warning dropped as a\n\t\t// duplicate is still one the consumer will see, so it still makes a restatement redundant.\n\t\treported: assessment.warnings.length > 0,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { AuthTokenInfo, AuthenticationError, AuthenticationErrorReason, RequestClientError, RequestNotFoundError, RequestRateLimitError } from "../../git/errors.js";
|
|
2
2
|
//#region ../plus/integrations/src/errors.d.ts
|
|
3
|
+
/** Transport wrappers keep the network failure in `cause` or `original`; inspecting only the wrapper loses it. */
|
|
4
|
+
export declare function isProviderUnreachableError(ex: unknown): boolean;
|
|
3
5
|
/**
|
|
4
6
|
* Thrown by a provider read when it can't produce a result for a reason that is NOT "the account is empty":
|
|
5
7
|
* a prerequisite is missing (e.g. Trello has no app key) or a required scope can't be resolved (e.g. the
|
|
@@ -1,5 +1,49 @@
|
|
|
1
|
+
import { isCancellationError } from "../../utils/cancellation.js";
|
|
1
2
|
import { AuthenticationError, AuthenticationErrorReason, RequestClientError, RequestNotFoundError, RequestRateLimitError } from "../../git/errors.js";
|
|
2
3
|
//#region ../plus/integrations/src/errors.ts
|
|
4
|
+
const unreachableErrorCodes = /* @__PURE__ */ new Set([
|
|
5
|
+
"ECONNREFUSED",
|
|
6
|
+
"ECONNRESET",
|
|
7
|
+
"ECONNABORTED",
|
|
8
|
+
"ENOTFOUND",
|
|
9
|
+
"EAI_AGAIN",
|
|
10
|
+
"ETIMEDOUT",
|
|
11
|
+
"EHOSTUNREACH",
|
|
12
|
+
"ENETUNREACH",
|
|
13
|
+
"EPIPE",
|
|
14
|
+
"ERR_NETWORK",
|
|
15
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
16
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
17
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
18
|
+
"UND_ERR_SOCKET",
|
|
19
|
+
"CERT_HAS_EXPIRED",
|
|
20
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
21
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
22
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
23
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
24
|
+
"ERR_TLS_CERT_ALTNAME_INVALID"
|
|
25
|
+
]);
|
|
26
|
+
/** Transport wrappers keep the network failure in `cause` or `original`; inspecting only the wrapper loses it. */
|
|
27
|
+
function isProviderUnreachableError(ex) {
|
|
28
|
+
const pending = [ex];
|
|
29
|
+
const seen = /* @__PURE__ */ new Set();
|
|
30
|
+
while (pending.length > 0) {
|
|
31
|
+
const candidate = pending.pop();
|
|
32
|
+
if (candidate == null || typeof candidate !== "object" || seen.has(candidate)) continue;
|
|
33
|
+
seen.add(candidate);
|
|
34
|
+
const error = candidate;
|
|
35
|
+
if (isCancellationError(candidate)) {
|
|
36
|
+
if (error.reason === "aborted") return false;
|
|
37
|
+
if (error.reason === "timeout") return true;
|
|
38
|
+
}
|
|
39
|
+
const status = error.status ?? error.response?.status;
|
|
40
|
+
if (typeof status === "number" && status >= 400) return status >= 500 && status < 600;
|
|
41
|
+
if (error.kind === "network" || error.reason === "unreachable" || error.name === "TimeoutError" || error.name === "AbortError" || error.name === "NetworkError" || typeof error.code === "string" && (unreachableErrorCodes.has(error.code) || error.code.startsWith("ERR_SSL_")) || typeof error.message === "string" && /^(?:fetch failed|failed to fetch|network request failed|load failed|NetworkError when attempting to fetch resource\.?)$/i.test(error.message)) return true;
|
|
42
|
+
pending.push(error.original, error.cause);
|
|
43
|
+
if (Array.isArray(error.errors)) pending.push(...error.errors);
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
3
47
|
/**
|
|
4
48
|
* Thrown by a provider read when it can't produce a result for a reason that is NOT "the account is empty":
|
|
5
49
|
* a prerequisite is missing (e.g. Trello has no app key) or a required scope can't be resolved (e.g. the
|
|
@@ -129,6 +173,6 @@ function toError(ex) {
|
|
|
129
173
|
return ex instanceof Error ? ex : new Error(String(ex));
|
|
130
174
|
}
|
|
131
175
|
//#endregion
|
|
132
|
-
export { AuthenticationError, AuthenticationErrorReason, IntegrationReadUnavailableError, ProviderFetchError, RequestClientError, RequestNotFoundError, RequestRateLimitError, getResponseHeader, isRateLimitResponse, toError, toRateLimitError };
|
|
176
|
+
export { AuthenticationError, AuthenticationErrorReason, IntegrationReadUnavailableError, ProviderFetchError, RequestClientError, RequestNotFoundError, RequestRateLimitError, getResponseHeader, isProviderUnreachableError, isRateLimitResponse, toError, toRateLimitError };
|
|
133
177
|
|
|
134
178
|
//# sourceMappingURL=errors.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","names":[],"sources":["../../../../plus/integrations/src/errors.ts"],"sourcesContent":["export type { AuthTokenInfo } from '@gitlens/git/errors.js';\nexport {\n\tAuthenticationError,\n\tAuthenticationErrorReason,\n\tRequestClientError,\n\tRequestNotFoundError,\n\tRequestRateLimitError,\n} from '@gitlens/git/errors.js';\n\nimport { RequestRateLimitError } from '@gitlens/git/errors.js';\n\n/**\n * Thrown by a provider read when it can't produce a result for a reason that is NOT \"the account is empty\":\n * a prerequisite is missing (e.g. Trello has no app key) or a required scope can't be resolved (e.g. the\n * Linear viewer id, needed to scope \"my issues\"). Returning an empty/undefined result in these cases would be\n * indistinguishable from a genuinely empty account; throwing this lets the result cores recover it into an\n * `{ error }` the facade surfaces as a warning + `fetchFailed`.\n */\nexport class IntegrationReadUnavailableError extends Error {\n\tconstructor(provider: string, reason: string) {\n\t\tsuper(`${provider} read unavailable: ${reason}`);\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\n/**\n * Thrown by integration provider HTTP clients when an upstream request fails.\n */\nexport class ProviderFetchError extends Error {\n\tget status(): number {\n\t\treturn this.response.status;\n\t}\n\n\tget statusText(): string {\n\t\treturn this.response.statusText;\n\t}\n\n\t/**\n\t * Builds the error with the upstream body folded into the message, for clients that hold a `Response` whose\n\t * body has not been read (i.e. every non-`ok` path). Use this rather than the constructor whenever the body is\n\t * still available: the status line alone cannot distinguish a throttled request from a permission failure on\n\t * the statuses hosts overload (see {@link isRateLimitResponse}), and only the body says which one it is.\n\t *\n\t * Never throws: the body is best-effort, so an unreadable, empty, or already-consumed one simply yields the\n\t * status-line-only message the constructor would have produced.\n\t */\n\tstatic async fromResponse(provider: string, response: Response): Promise<ProviderFetchError> {\n\t\treturn new ProviderFetchError(provider, response, await readErrorBodyMessage(response));\n\t}\n\n\tconstructor(\n\t\tprovider: string,\n\t\tpublic readonly response: Response,\n\t\terrors?: { message: string }[],\n\t) {\n\t\tsuper(\n\t\t\t`${provider} request failed: ${!response.ok ? `(${response.status}) ${response.statusText}. ` : ''}${\n\t\t\t\terrors?.length ? errors[0].message : ''\n\t\t\t}`,\n\t\t);\n\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\n/** Cap on the body text folded into an error message, so an HTML error page can't become the message. */\nconst maxErrorBodyLength = 500;\n\n/**\n * Best-effort human-readable message from an error response body, in the `errors` shape the\n * {@link ProviderFetchError} constructor takes. Tolerant by design — every host in use words its errors\n * differently and some do not send JSON at all:\n * - Azure DevOps — `{ message: \"TF400733: ...\" }`\n * - Bitbucket — `{ error: { message: \"...\" } }`, and bare text for a throttled request\n * - GitLab — `{ message: \"...\" }` or `{ error: \"...\" }`\n * Anything unrecognized falls back to the raw text, which is what the substring check wants anyway.\n */\nasync function readErrorBodyMessage(response: Response): Promise<{ message: string }[] | undefined> {\n\tlet text;\n\ttry {\n\t\t// `bodyUsed` is not a guarantee (a stream can fail mid-read), hence the catch — but it avoids the throw\n\t\t// entirely on the common path where something upstream already consumed the body.\n\t\tif (response.bodyUsed) return undefined;\n\n\t\ttext = (await response.text())?.trim();\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!text) return undefined;\n\n\tlet message: string | undefined;\n\ttry {\n\t\tconst body: unknown = JSON.parse(text);\n\t\tif (body != null && typeof body === 'object') {\n\t\t\tconst { message: m, error } = body as { message?: unknown; error?: unknown };\n\t\t\tif (typeof m === 'string') {\n\t\t\t\tmessage = m;\n\t\t\t} else if (typeof error === 'string') {\n\t\t\t\tmessage = error;\n\t\t\t} else if (error != null && typeof error === 'object') {\n\t\t\t\t// Some hosts nest it as `{ error: { message } }`; narrow the same way as the outer object.\n\t\t\t\tconst nested = (error as { message?: unknown }).message;\n\t\t\t\tif (typeof nested === 'string') {\n\t\t\t\t\tmessage = nested;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Not JSON — Bitbucket sends bare text for a throttled request, and any host can send an HTML error page.\n\t}\n\n\tmessage = (message ?? text).trim();\n\treturn message ? [{ message: message.slice(0, maxErrorBodyLength) }] : undefined;\n}\n\n/**\n * Whether a provider's error response denotes a rate limit rather than a permission failure. Kept here, in one\n * place, because the distinction is consequential: a rate limit is retryable, while a permission failure is\n * surfaced as `auth` and drives a reconnect prompt — misclassifying one as the other asks the user to\n * re-authenticate a perfectly healthy connection.\n *\n * Which status a throttled request arrives on varies by host, which is why this takes both:\n * - Azure DevOps and Bitbucket Cloud document 429 (learn.microsoft.com .../integrate/concepts/rate-limits).\n * - GitHub documents \"a `403` or `429` response\" for both its primary and secondary limits\n * (docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api).\n * - GitLab uses 403 for some throttles.\n * So 429 alone is decisive, and on 403 the message decides.\n *\n * NOTE the message must actually carry the host's body for the 403 path to work — see\n * {@link ProviderFetchError.fromResponse}. A `ProviderFetchError` built from the status line alone reads\n * \"(403) Forbidden.\" and can never match, which is what made this check dead code on the direct-fetch clients.\n */\nexport function isRateLimitResponse(ex: { status: number; message?: string }): boolean {\n\t// Matches the phrasing hosts actually use: GitHub/Bitbucket \"API rate limit exceeded\", GitLab \"rate limit\n\t// exceeded\", Azure \"Request was blocked due to exceeding usage of resource 'RateLimit'\" / \"TF400733: The\n\t// request has been canceled: Request was blocked due to exceeding usage\".\n\tif (ex.status === 429) return true;\n\n\tconst message = ex.message?.toLowerCase();\n\treturn message != null && (message.includes('rate limit') || message.includes('ratelimit'));\n}\n\n/**\n * A response's headers as either a `fetch` `Headers` instance or the plain object an SDK error carries. Read via\n * duck-typing on `get` rather than `instanceof Headers`, so this works in both the Node and webworker builds\n * without depending on the global being present.\n */\nexport type ResponseHeaders = Pick<Headers, 'get'> | Record<string, unknown>;\n\n/** Reads one header off either shape of {@link ResponseHeaders}; names are the lower-case ones both shapes use. */\nexport function getResponseHeader(headers: ResponseHeaders | undefined, name: string): unknown {\n\tif (headers == null) return undefined;\n\n\treturn typeof headers.get === 'function'\n\t\t? (headers.get as (name: string) => string | null)(name)\n\t\t: (headers as Record<string, unknown>)[name];\n}\n\n/**\n * Builds a {@link RequestRateLimitError} from a provider error response, reading the reset epoch from the\n * standard `x-ratelimit-reset` header when present. Accepts both header shapes in use: the `fetch` `Headers`\n * instance the direct clients get, and the plain object the SDK attaches to its errors.\n */\nexport function toRateLimitError(\n\tex: Error & { response?: { headers?: ResponseHeaders } },\n\ttoken: string | undefined,\n): RequestRateLimitError {\n\tconst raw = getResponseHeader(ex.response?.headers, 'x-ratelimit-reset');\n\n\t// Only a header that actually came back as a string/number can be a reset epoch; anything else (a nested SDK\n\t// object, an array of values) would stringify to garbage and parse to NaN anyway.\n\tlet resetAt: number | undefined;\n\tif (typeof raw === 'string' || typeof raw === 'number') {\n\t\tresetAt = parseInt(String(raw), 10);\n\t\tif (Number.isNaN(resetAt)) {\n\t\t\tresetAt = undefined;\n\t\t}\n\t}\n\n\treturn new RequestRateLimitError(ex, token, resetAt);\n}\n\n/**\n * Normalizes a caught `unknown` into an `Error`. Result cores type their failure channel as `error: Error`,\n * but a `catch` binding is `unknown` and a provider (or a third-party SDK) can throw a non-Error value;\n * returning it raw would leak a non-Error through the public result surface and break downstream `instanceof`\n * classification. Passes an existing `Error` through unchanged and wraps anything else with `String(ex)`.\n */\nexport function toError(ex: unknown): Error {\n\treturn ex instanceof Error ? ex : new Error(String(ex));\n}\n"],"mappings":";;;;;;;;;AAkBA,IAAa,kCAAb,cAAqD,MAAM;CAC1D,YAAY,UAAkB,QAAgB;EAC7C,MAAM,GAAG,SAAS,qBAAqB,QAAQ;EAC/C,MAAM,oBAAoB,MAAM,UAAU;CAC3C;AACD;;;;AAKA,IAAa,qBAAb,MAAa,2BAA2B,MAAM;CAwB5B;CAvBjB,IAAI,SAAiB;EACpB,OAAO,KAAK,SAAS;CACtB;CAEA,IAAI,aAAqB;EACxB,OAAO,KAAK,SAAS;CACtB;;;;;;;;;;CAWA,aAAa,aAAa,UAAkB,UAAiD;EAC5F,OAAO,IAAI,mBAAmB,UAAU,UAAU,MAAM,qBAAqB,QAAQ,CAAC;CACvF;CAEA,YACC,UACA,UACA,QACC;EACD,MACC,GAAG,SAAS,mBAAmB,CAAC,SAAS,KAAK,IAAI,SAAS,OAAO,IAAI,SAAS,WAAW,MAAM,KAC/F,QAAQ,SAAS,OAAO,EAAE,CAAC,UAAU,IAEvC;EAPgB,KAAA,WAAA;EAShB,MAAM,oBAAoB,MAAM,UAAU;CAC3C;AACD;;AAGA,MAAM,qBAAqB;;;;;;;;;;AAW3B,eAAe,qBAAqB,UAAgE;CACnG,IAAI;CACJ,IAAI;EAGH,IAAI,SAAS,UAAU,OAAO,KAAA;EAE9B,QAAQ,MAAM,SAAS,KAAK,EAAA,EAAI,KAAK;CACtC,QAAQ;EACP;CACD;CACA,IAAI,CAAC,MAAM,OAAO,KAAA;CAElB,IAAI;CACJ,IAAI;EACH,MAAM,OAAgB,KAAK,MAAM,IAAI;EACrC,IAAI,QAAQ,QAAQ,OAAO,SAAS,UAAU;GAC7C,MAAM,EAAE,SAAS,GAAG,UAAU;GAC9B,IAAI,OAAO,MAAM,UAChB,UAAU;QACJ,IAAI,OAAO,UAAU,UAC3B,UAAU;QACJ,IAAI,SAAS,QAAQ,OAAO,UAAU,UAAU;IAEtD,MAAM,SAAU,MAAgC;IAChD,IAAI,OAAO,WAAW,UACrB,UAAU;GAEZ;EACD;CACD,QAAQ,CAER;CAEA,WAAW,WAAW,KAAA,CAAM,KAAK;CACjC,OAAO,UAAU,CAAC,EAAE,SAAS,QAAQ,MAAM,GAAG,kBAAkB,EAAE,CAAC,IAAI,KAAA;AACxE;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,oBAAoB,IAAmD;CAItF,IAAI,GAAG,WAAW,KAAK,OAAO;CAE9B,MAAM,UAAU,GAAG,SAAS,YAAY;CACxC,OAAO,WAAW,SAAS,QAAQ,SAAS,YAAY,KAAK,QAAQ,SAAS,WAAW;AAC1F;;AAUA,SAAgB,kBAAkB,SAAsC,MAAuB;CAC9F,IAAI,WAAW,MAAM,OAAO,KAAA;CAE5B,OAAO,OAAO,QAAQ,QAAQ,aAC1B,QAAQ,IAAwC,IAAI,IACpD,QAAoC;AACzC;;;;;;AAOA,SAAgB,iBACf,IACA,OACwB;CACxB,MAAM,MAAM,kBAAkB,GAAG,UAAU,SAAS,mBAAmB;CAIvE,IAAI;CACJ,IAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ,UAAU;EACvD,UAAU,SAAS,OAAO,GAAG,GAAG,EAAE;EAClC,IAAI,OAAO,MAAM,OAAO,GACvB,UAAU,KAAA;CAEZ;CAEA,OAAO,IAAI,sBAAsB,IAAI,OAAO,OAAO;AACpD;;;;;;;AAQA,SAAgB,QAAQ,IAAoB;CAC3C,OAAO,cAAc,QAAQ,KAAK,IAAI,MAAM,OAAO,EAAE,CAAC;AACvD"}
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../../../../plus/integrations/src/errors.ts"],"sourcesContent":["export type { AuthTokenInfo } from '@gitlens/git/errors.js';\nexport {\n\tAuthenticationError,\n\tAuthenticationErrorReason,\n\tRequestClientError,\n\tRequestNotFoundError,\n\tRequestRateLimitError,\n} from '@gitlens/git/errors.js';\n\nimport { RequestRateLimitError } from '@gitlens/git/errors.js';\nimport { isCancellationError } from '@gitlens/utils/cancellation.js';\n\nconst unreachableErrorCodes = new Set([\n\t'ECONNREFUSED',\n\t'ECONNRESET',\n\t'ECONNABORTED',\n\t'ENOTFOUND',\n\t'EAI_AGAIN',\n\t'ETIMEDOUT',\n\t'EHOSTUNREACH',\n\t'ENETUNREACH',\n\t'EPIPE',\n\t'ERR_NETWORK',\n\t'UND_ERR_CONNECT_TIMEOUT',\n\t'UND_ERR_HEADERS_TIMEOUT',\n\t'UND_ERR_BODY_TIMEOUT',\n\t'UND_ERR_SOCKET',\n\t'CERT_HAS_EXPIRED',\n\t'DEPTH_ZERO_SELF_SIGNED_CERT',\n\t'SELF_SIGNED_CERT_IN_CHAIN',\n\t'UNABLE_TO_VERIFY_LEAF_SIGNATURE',\n\t'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',\n\t'ERR_TLS_CERT_ALTNAME_INVALID',\n]);\n\n/** Transport wrappers keep the network failure in `cause` or `original`; inspecting only the wrapper loses it. */\nexport function isProviderUnreachableError(ex: unknown): boolean {\n\tconst pending = [ex];\n\tconst seen = new Set<unknown>();\n\twhile (pending.length > 0) {\n\t\tconst candidate = pending.pop();\n\t\tif (candidate == null || typeof candidate !== 'object' || seen.has(candidate)) continue;\n\n\t\tseen.add(candidate);\n\t\tconst error = candidate as {\n\t\t\tname?: string;\n\t\t\tkind?: string;\n\t\t\tmessage?: string;\n\t\t\tcode?: string;\n\t\t\tstatus?: number;\n\t\t\tresponse?: { status?: number };\n\t\t\treason?: string;\n\t\t\toriginal?: unknown;\n\t\t\tcause?: unknown;\n\t\t\terrors?: unknown[];\n\t\t};\n\t\tif (isCancellationError(candidate)) {\n\t\t\tif (error.reason === 'aborted') return false;\n\t\t\tif (error.reason === 'timeout') return true;\n\t\t}\n\n\t\tconst status = error.status ?? error.response?.status;\n\t\tif (typeof status === 'number' && status >= 400) return status >= 500 && status < 600;\n\n\t\tif (\n\t\t\terror.kind === 'network' ||\n\t\t\terror.reason === 'unreachable' ||\n\t\t\terror.name === 'TimeoutError' ||\n\t\t\terror.name === 'AbortError' ||\n\t\t\terror.name === 'NetworkError' ||\n\t\t\t(typeof error.code === 'string' &&\n\t\t\t\t(unreachableErrorCodes.has(error.code) || error.code.startsWith('ERR_SSL_'))) ||\n\t\t\t(typeof error.message === 'string' &&\n\t\t\t\t/^(?:fetch failed|failed to fetch|network request failed|load failed|NetworkError when attempting to fetch resource\\.?)$/i.test(\n\t\t\t\t\terror.message,\n\t\t\t\t))\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\n\t\tpending.push(error.original, error.cause);\n\t\tif (Array.isArray(error.errors)) {\n\t\t\tpending.push(...error.errors);\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Thrown by a provider read when it can't produce a result for a reason that is NOT \"the account is empty\":\n * a prerequisite is missing (e.g. Trello has no app key) or a required scope can't be resolved (e.g. the\n * Linear viewer id, needed to scope \"my issues\"). Returning an empty/undefined result in these cases would be\n * indistinguishable from a genuinely empty account; throwing this lets the result cores recover it into an\n * `{ error }` the facade surfaces as a warning + `fetchFailed`.\n */\nexport class IntegrationReadUnavailableError extends Error {\n\tconstructor(provider: string, reason: string) {\n\t\tsuper(`${provider} read unavailable: ${reason}`);\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\n/**\n * Thrown by integration provider HTTP clients when an upstream request fails.\n */\nexport class ProviderFetchError extends Error {\n\tget status(): number {\n\t\treturn this.response.status;\n\t}\n\n\tget statusText(): string {\n\t\treturn this.response.statusText;\n\t}\n\n\t/**\n\t * Builds the error with the upstream body folded into the message, for clients that hold a `Response` whose\n\t * body has not been read (i.e. every non-`ok` path). Use this rather than the constructor whenever the body is\n\t * still available: the status line alone cannot distinguish a throttled request from a permission failure on\n\t * the statuses hosts overload (see {@link isRateLimitResponse}), and only the body says which one it is.\n\t *\n\t * Never throws: the body is best-effort, so an unreadable, empty, or already-consumed one simply yields the\n\t * status-line-only message the constructor would have produced.\n\t */\n\tstatic async fromResponse(provider: string, response: Response): Promise<ProviderFetchError> {\n\t\treturn new ProviderFetchError(provider, response, await readErrorBodyMessage(response));\n\t}\n\n\tconstructor(\n\t\tprovider: string,\n\t\tpublic readonly response: Response,\n\t\terrors?: { message: string }[],\n\t) {\n\t\tsuper(\n\t\t\t`${provider} request failed: ${!response.ok ? `(${response.status}) ${response.statusText}. ` : ''}${\n\t\t\t\terrors?.length ? errors[0].message : ''\n\t\t\t}`,\n\t\t);\n\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\n/** Cap on the body text folded into an error message, so an HTML error page can't become the message. */\nconst maxErrorBodyLength = 500;\n\n/**\n * Best-effort human-readable message from an error response body, in the `errors` shape the\n * {@link ProviderFetchError} constructor takes. Tolerant by design — every host in use words its errors\n * differently and some do not send JSON at all:\n * - Azure DevOps — `{ message: \"TF400733: ...\" }`\n * - Bitbucket — `{ error: { message: \"...\" } }`, and bare text for a throttled request\n * - GitLab — `{ message: \"...\" }` or `{ error: \"...\" }`\n * Anything unrecognized falls back to the raw text, which is what the substring check wants anyway.\n */\nasync function readErrorBodyMessage(response: Response): Promise<{ message: string }[] | undefined> {\n\tlet text;\n\ttry {\n\t\t// `bodyUsed` is not a guarantee (a stream can fail mid-read), hence the catch — but it avoids the throw\n\t\t// entirely on the common path where something upstream already consumed the body.\n\t\tif (response.bodyUsed) return undefined;\n\n\t\ttext = (await response.text())?.trim();\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!text) return undefined;\n\n\tlet message: string | undefined;\n\ttry {\n\t\tconst body: unknown = JSON.parse(text);\n\t\tif (body != null && typeof body === 'object') {\n\t\t\tconst { message: m, error } = body as { message?: unknown; error?: unknown };\n\t\t\tif (typeof m === 'string') {\n\t\t\t\tmessage = m;\n\t\t\t} else if (typeof error === 'string') {\n\t\t\t\tmessage = error;\n\t\t\t} else if (error != null && typeof error === 'object') {\n\t\t\t\t// Some hosts nest it as `{ error: { message } }`; narrow the same way as the outer object.\n\t\t\t\tconst nested = (error as { message?: unknown }).message;\n\t\t\t\tif (typeof nested === 'string') {\n\t\t\t\t\tmessage = nested;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Not JSON — Bitbucket sends bare text for a throttled request, and any host can send an HTML error page.\n\t}\n\n\tmessage = (message ?? text).trim();\n\treturn message ? [{ message: message.slice(0, maxErrorBodyLength) }] : undefined;\n}\n\n/**\n * Whether a provider's error response denotes a rate limit rather than a permission failure. Kept here, in one\n * place, because the distinction is consequential: a rate limit is retryable, while a permission failure is\n * surfaced as `auth` and drives a reconnect prompt — misclassifying one as the other asks the user to\n * re-authenticate a perfectly healthy connection.\n *\n * Which status a throttled request arrives on varies by host, which is why this takes both:\n * - Azure DevOps and Bitbucket Cloud document 429 (learn.microsoft.com .../integrate/concepts/rate-limits).\n * - GitHub documents \"a `403` or `429` response\" for both its primary and secondary limits\n * (docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api).\n * - GitLab uses 403 for some throttles.\n * So 429 alone is decisive, and on 403 the message decides.\n *\n * NOTE the message must actually carry the host's body for the 403 path to work — see\n * {@link ProviderFetchError.fromResponse}. A `ProviderFetchError` built from the status line alone reads\n * \"(403) Forbidden.\" and can never match, which is what made this check dead code on the direct-fetch clients.\n */\nexport function isRateLimitResponse(ex: { status: number; message?: string }): boolean {\n\t// Matches the phrasing hosts actually use: GitHub/Bitbucket \"API rate limit exceeded\", GitLab \"rate limit\n\t// exceeded\", Azure \"Request was blocked due to exceeding usage of resource 'RateLimit'\" / \"TF400733: The\n\t// request has been canceled: Request was blocked due to exceeding usage\".\n\tif (ex.status === 429) return true;\n\n\tconst message = ex.message?.toLowerCase();\n\treturn message != null && (message.includes('rate limit') || message.includes('ratelimit'));\n}\n\n/**\n * A response's headers as either a `fetch` `Headers` instance or the plain object an SDK error carries. Read via\n * duck-typing on `get` rather than `instanceof Headers`, so this works in both the Node and webworker builds\n * without depending on the global being present.\n */\nexport type ResponseHeaders = Pick<Headers, 'get'> | Record<string, unknown>;\n\n/** Reads one header off either shape of {@link ResponseHeaders}; names are the lower-case ones both shapes use. */\nexport function getResponseHeader(headers: ResponseHeaders | undefined, name: string): unknown {\n\tif (headers == null) return undefined;\n\n\treturn typeof headers.get === 'function'\n\t\t? (headers.get as (name: string) => string | null)(name)\n\t\t: (headers as Record<string, unknown>)[name];\n}\n\n/**\n * Builds a {@link RequestRateLimitError} from a provider error response, reading the reset epoch from the\n * standard `x-ratelimit-reset` header when present. Accepts both header shapes in use: the `fetch` `Headers`\n * instance the direct clients get, and the plain object the SDK attaches to its errors.\n */\nexport function toRateLimitError(\n\tex: Error & { response?: { headers?: ResponseHeaders } },\n\ttoken: string | undefined,\n): RequestRateLimitError {\n\tconst raw = getResponseHeader(ex.response?.headers, 'x-ratelimit-reset');\n\n\t// Only a header that actually came back as a string/number can be a reset epoch; anything else (a nested SDK\n\t// object, an array of values) would stringify to garbage and parse to NaN anyway.\n\tlet resetAt: number | undefined;\n\tif (typeof raw === 'string' || typeof raw === 'number') {\n\t\tresetAt = parseInt(String(raw), 10);\n\t\tif (Number.isNaN(resetAt)) {\n\t\t\tresetAt = undefined;\n\t\t}\n\t}\n\n\treturn new RequestRateLimitError(ex, token, resetAt);\n}\n\n/**\n * Normalizes a caught `unknown` into an `Error`. Result cores type their failure channel as `error: Error`,\n * but a `catch` binding is `unknown` and a provider (or a third-party SDK) can throw a non-Error value;\n * returning it raw would leak a non-Error through the public result surface and break downstream `instanceof`\n * classification. Passes an existing `Error` through unchanged and wraps anything else with `String(ex)`.\n */\nexport function toError(ex: unknown): Error {\n\treturn ex instanceof Error ? ex : new Error(String(ex));\n}\n"],"mappings":"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"}
|
|
@@ -934,6 +934,7 @@ var IntegrationService = class {
|
|
|
934
934
|
const syncedPrimaryIdsByDomain = /* @__PURE__ */ new Map();
|
|
935
935
|
const existingById = new Map(this.configuredIntegrationService.getConfigured(id, { cloud: true }).map((c) => [c.id, c]));
|
|
936
936
|
const repointedDomains = /* @__PURE__ */ new Set();
|
|
937
|
+
const signOutMark = this.configuredIntegrationService.getSignOutMark();
|
|
937
938
|
const preparedConnections = await Promise.all(identified.map(async (connection) => {
|
|
938
939
|
const host = hostFromDomain(connection.domain);
|
|
939
940
|
if (isSelfManagedHostIntegrationId(id) && !host) {
|
|
@@ -976,7 +977,7 @@ var IntegrationService = class {
|
|
|
976
977
|
}));
|
|
977
978
|
for (const prepared of preparedConnections) {
|
|
978
979
|
if (prepared == null) continue;
|
|
979
|
-
if (prepared.kind === "fetched"
|
|
980
|
+
if (prepared.kind === "fetched" && !await this.configuredIntegrationService.storeSession(id, prepared.providerSession, { signOutMark })) continue;
|
|
980
981
|
syncedIds.add(prepared.connection.id);
|
|
981
982
|
if (prepared.connection.primary) {
|
|
982
983
|
const domain = isSelfManagedHostIntegrationId(id) ? prepared.host : void 0;
|