@gitkraken/core-gitlens 0.20.2 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/dist/git/models/issue.d.ts +30 -1
  3. package/dist/git/models/issue.js +3 -1
  4. package/dist/git/models/issue.js.map +1 -1
  5. package/dist/git/utils/autolink.utils.d.ts +4 -1
  6. package/dist/git/utils/autolink.utils.js +12 -3
  7. package/dist/git/utils/autolink.utils.js.map +1 -1
  8. package/dist/git/utils/issue.utils.js +2 -1
  9. package/dist/git/utils/issue.utils.js.map +1 -1
  10. package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts +7 -0
  11. package/dist/plus/integrations/authentication/configuredIntegrationService.js +12 -0
  12. package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
  13. package/dist/plus/integrations/authentication/integrationAuthenticationService.js +5 -3
  14. package/dist/plus/integrations/authentication/integrationAuthenticationService.js.map +1 -1
  15. package/dist/plus/integrations/authentication/models.d.ts +9 -0
  16. package/dist/plus/integrations/authentication/models.js +13 -1
  17. package/dist/plus/integrations/authentication/models.js.map +1 -1
  18. package/dist/plus/integrations/authentication/observedAuthenticationProvider.d.ts +34 -0
  19. package/dist/plus/integrations/authentication/observedAuthenticationProvider.js +227 -0
  20. package/dist/plus/integrations/authentication/observedAuthenticationProvider.js.map +1 -0
  21. package/dist/plus/integrations/index.d.ts +4 -2
  22. package/dist/plus/integrations/index.js.map +1 -1
  23. package/dist/plus/integrations/integrationService.d.ts +9 -4
  24. package/dist/plus/integrations/integrationService.js +30 -7
  25. package/dist/plus/integrations/integrationService.js.map +1 -1
  26. package/dist/plus/integrations/manager.d.ts +18 -1
  27. package/dist/plus/integrations/models/integration.d.ts +21 -2
  28. package/dist/plus/integrations/models/integration.js +87 -11
  29. package/dist/plus/integrations/models/integration.js.map +1 -1
  30. package/dist/plus/integrations/models/issuesIntegration.d.ts +20 -1
  31. package/dist/plus/integrations/models/issuesIntegration.js +106 -2
  32. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  33. package/dist/plus/integrations/providers/jira-server.d.ts +10 -2
  34. package/dist/plus/integrations/providers/jira-server.js +74 -7
  35. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  36. package/dist/plus/integrations/providers/jira.d.ts +1 -1
  37. package/dist/plus/integrations/providers/jira.js +3 -25
  38. package/dist/plus/integrations/providers/jira.js.map +1 -1
  39. package/dist/plus/integrations/providers/linear.d.ts +1 -1
  40. package/dist/plus/integrations/providers/linear.js +3 -27
  41. package/dist/plus/integrations/providers/linear.js.map +1 -1
  42. package/dist/plus/integrations/providers/linearIssuesEtag.d.ts +20 -4
  43. package/dist/plus/integrations/providers/linearIssuesEtag.js +42 -6
  44. package/dist/plus/integrations/providers/linearIssuesEtag.js.map +1 -1
  45. package/dist/plus/integrations/providers/models.js +15 -2
  46. package/dist/plus/integrations/providers/models.js.map +1 -1
  47. package/dist/plus/integrations/providers/providerErrors.d.ts +2 -0
  48. package/dist/plus/integrations/providers/providerErrors.js +5 -2
  49. package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
  50. package/dist/plus/integrations/providers/trello.d.ts +1 -1
  51. package/dist/plus/integrations/providers/trello.js +1 -1
  52. package/dist/plus/integrations/providers/trello.js.map +1 -1
  53. package/dist/plus/integrations/providers/utils/issueAutolinks.d.ts +8 -0
  54. package/dist/plus/integrations/providers/utils/issueAutolinks.js +20 -0
  55. package/dist/plus/integrations/providers/utils/issueAutolinks.js.map +1 -0
  56. package/dist/plus/integrations/reads/autolinks.d.ts +7 -0
  57. package/dist/plus/integrations/reads/autolinks.js +40 -0
  58. package/dist/plus/integrations/reads/autolinks.js.map +1 -0
  59. package/dist/plus/integrations/reads/context.d.ts +4 -2
  60. package/dist/plus/integrations/reads/etag.js +3 -1
  61. package/dist/plus/integrations/reads/etag.js.map +1 -1
  62. package/dist/plus/integrations/reads/hierarchy.js +1 -1
  63. package/dist/plus/integrations/reads/issueBatch.js +1 -1
  64. package/dist/plus/integrations/reads/issueTracker.js +1 -1
  65. package/dist/plus/integrations/reads/resolveRepository.js +1 -1
  66. package/docs/integrations.md +34 -2
  67. package/package.json +2 -2
@@ -147,7 +147,10 @@ function isGitHubProviderId(providerId) {
147
147
  const githubGraphQLInt32Max = 2147483647;
148
148
  function isLinearRateLimitError(providerId, ex) {
149
149
  if (providerId !== "linear") return false;
150
- const body = ex.response?.body;
150
+ return isLinearRateLimitedBody(ex.response?.body);
151
+ }
152
+ /** Whether a Linear GraphQL response body reports throttling. */
153
+ function isLinearRateLimitedBody(body) {
151
154
  return getProviderGraphQLErrors(body).some((error) => error.extensions?.code === "RATELIMITED");
152
155
  }
153
156
  function isProviderIssueNotFoundError(providerId, ex) {
@@ -240,6 +243,6 @@ function getProviderResponseBodyMessage(body) {
240
243
  return message ? message.slice(0, maxProviderErrorBodyLength) : void 0;
241
244
  }
242
245
  //#endregion
243
- export { AnonymousResponseError, UnexpectedHtmlResponseError, UnexpectedResponseError, getProviderResponseBodyMessage, githubGraphQLInt32Max, isAzureProviderId, isGitHubProviderId, isJiraMissingProjectError, isJiraServerMissingProjectError, isProviderIssueNotFoundError, markRequestError, throwProviderError };
246
+ export { AnonymousResponseError, UnexpectedHtmlResponseError, UnexpectedResponseError, getProviderResponseBodyMessage, githubGraphQLInt32Max, isAzureProviderId, isGitHubProviderId, isJiraMissingProjectError, isJiraServerMissingProjectError, isLinearRateLimitedBody, isProviderIssueNotFoundError, markRequestError, throwProviderError };
244
247
 
245
248
  //# sourceMappingURL=providerErrors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"providerErrors.js","names":[],"sources":["../../../../../plus/integrations/src/providers/providerErrors.ts"],"sourcesContent":["import { Logger } from '@gitlens/utils/logger.js';\nimport type { TokenWithInfo } from '../authentication/models.js';\nimport type { IntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tIssuesCloudHostIntegrationId,\n} from '../constants.js';\nimport type { ResponseHeaders } from '../errors.js';\nimport {\n\tAuthenticationError,\n\tAuthenticationErrorReason,\n\tgetAuthenticatedUser,\n\tisProviderUnreachableError,\n\tisRateLimitResponse,\n\tRequestClientError,\n\tRequestNotFoundError,\n\ttoError,\n\ttoRateLimitError,\n} from '../errors.js';\n\nconst maxProviderErrorBodyLength = 500;\n\n/**\n * A 2xx response carrying an HTML page instead of data.\n *\n * Azure DevOps answers a rejected credential by redirecting to its sign-in page, which then returns\n * `203 text/html` rather than `401`. `response.ok` spans the whole 200-299 range, so that page used to be read as\n * a successful body and handed to provider code typed as the JSON it is not — surfacing as an opaque `TypeError`\n * deep inside a provider, or as nothing at all on a route that returns `void`, which made a bad credential look\n * like a successful write (GKDEV-3617).\n *\n * `provider-apis` 0.59.0 raises its own error for this, but only from the transport it wraps; this package passes\n * its own already-parsed request function, so the check has to live here too.\n *\n * The message carries no URL, and the attached response is stripped of its credential-bearing headers and cut to\n * a diagnostic prefix: an error is something a consumer may reasonably log or forward, and the response it is\n * built from is a sign-in page carrying a session cookie.\n */\nexport class UnexpectedHtmlResponseError extends Error {\n\tstatic is(ex: unknown): ex is UnexpectedHtmlResponseError {\n\t\treturn ex instanceof UnexpectedHtmlResponseError;\n\t}\n\n\treadonly response: SanitizedErrorResponse;\n\n\tconstructor(status: number, contentType: string, response: ProviderErrorResponse | undefined) {\n\t\tsuper(`(${status}) Expected data but the provider returned an HTML page (content-type: ${contentType})`);\n\n\t\tthis.response = sanitizeErrorResponse(status, response);\n\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\n/**\n * A provider read that failed on the data a request returned rather than on the request: `provider-apis` reads a\n * response body as the shape it expects without checking it, so a 2xx carrying anything else (a gateway's JSON\n * error, `{}`, `null`, a GraphQL answer with neither data nor errors) surfaces as a `TypeError` thrown deep inside the\n * read, such as `response.body.map is not a function` from Jira Cloud's site list or Trello's boards, or\n * `Cannot read properties of undefined (reading 'id')` from Linear's organization. See {@link throwProviderError}.\n *\n * The message is the readable one a warning carries. The `TypeError` is kept as `original`, and a log line, which\n * prints the error through `toString()`, names it too.\n */\nexport class UnexpectedResponseError extends Error {\n\tstatic is(ex: unknown): ex is UnexpectedResponseError {\n\t\treturn ex instanceof UnexpectedResponseError;\n\t}\n\n\tconstructor(readonly original: Error) {\n\t\tsuper('Expected data but the provider returned an unexpected response');\n\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n\n\toverride toString(): string {\n\t\treturn `${super.toString()} (${String(this.original)})`;\n\t}\n}\n\n/** What the request function `ProvidersApi` hands `provider-apis` threw itself: see {@link markRequestError}. */\nconst requestErrors = new WeakSet<object>();\n\n/**\n * Records an error the request function threw itself (a transport failure, or a response it refused), so\n * {@link throwProviderError} never reads it as data a read could not handle, whatever the host's fetch throws.\n * Returns `ex`, to rethrow.\n */\nexport function markRequestError(ex: unknown): unknown {\n\tif (ex != null && typeof ex === 'object') {\n\t\trequestErrors.add(ex);\n\t}\n\treturn ex;\n}\n\n/**\n * A response that names the anonymous user (`X-AUSERNAME: anonymous`) to a request that carried a credential.\n *\n * Jira Data Center serves a token it rejects as the anonymous user rather than refusing it: measured against Jira\n * Data Center 10.7.3 with a revoked token, `/rest/api/2/project` answers `200 []` and a project search answers the\n * same `400` it gives a deleted project. Read as data, that is an account with no projects, and a selected project\n * looks deleted. The server only names `anonymous` when the credential did not authenticate, so the response is the\n * credential's rejection whatever its status, except a 401, which already fails as one, and a 429 or a 5xx, which\n * prove nothing about the credential.\n *\n * Its response keeps only the status and headers, stripped like {@link UnexpectedHtmlResponseError}'s.\n */\nexport class AnonymousResponseError extends Error {\n\tstatic is(ex: unknown): ex is AnonymousResponseError {\n\t\treturn ex instanceof AnonymousResponseError;\n\t}\n\n\treadonly response: SanitizedErrorResponse;\n\n\tconstructor(status: number, response: ProviderErrorResponse | undefined) {\n\t\tsuper(`(${status}) The provider served a request sent with a credential as the anonymous user`);\n\n\t\tthis.response = sanitizeErrorResponse(status, response);\n\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\ntype ProviderErrorResponse = { body?: unknown; headers?: Record<string, string>; status?: number };\ntype SanitizedErrorResponse = { body?: string; headers: Record<string, string>; status: number };\n\n/**\n * Response headers a rejected-credential page carries that must not ride along on an error: the session cookie it\n * sets, the challenges whose base64 payload decodes to the tenant or the internal domain, and the request-side\n * names a substituted fetch can echo back onto a response.\n */\nconst unloggableResponseHeaders = new Set([\n\t'authentication-info',\n\t'authorization',\n\t'cookie',\n\t'proxy-authenticate',\n\t'proxy-authentication-info',\n\t'proxy-authorization',\n\t'set-cookie',\n\t'www-authenticate',\n\t'x-api-key',\n\t'x-auth-token',\n]);\n\n/** Strips the credential-bearing headers from a response and cuts its body to a diagnostic prefix. */\nfunction sanitizeErrorResponse(status: number, response: ProviderErrorResponse | undefined): SanitizedErrorResponse {\n\tconst headers: Record<string, string> = {};\n\tfor (const [name, value] of Object.entries(response?.headers ?? {})) {\n\t\tif (unloggableResponseHeaders.has(name.toLowerCase())) continue;\n\n\t\theaders[name] = value;\n\t}\n\n\tconst body = response?.body;\n\treturn {\n\t\t// A sign-in page runs to tens of kilobytes and none of it is a diagnostic; keep only what identifies it.\n\t\t...(typeof body === 'string' ? { body: body.slice(0, maxProviderErrorBodyLength) } : {}),\n\t\theaders: headers,\n\t\tstatus: response?.status ?? status,\n\t};\n}\n\nconst linearIssueNotFoundMessage = /^Linear issue not found: .+$/i;\n\n/**\n * What Linear actually answers for an identifier it can't resolve: HTTP 200 with the single GraphQL error\n * `Entity not found: Issue` (`INPUT_ERROR`), which provider-apis rethrows as `Linear GraphQL errors: <messages>`, the\n * response not attached. Only that one message proves the miss; any other error, or another alongside it, is a\n * failure.\n */\nconst linearIssueEntityNotFoundMessage = /^Linear GraphQL errors: Entity not found: Issue$/;\n\ntype ProviderGraphQLError = {\n\tmessage?: unknown;\n\textensions?: { code?: unknown };\n};\n\nfunction getProviderGraphQLErrors(body: unknown): ProviderGraphQLError[] {\n\tif (body == null || typeof body !== 'object') return [];\n\n\tconst errors = (body as { errors?: unknown }).errors;\n\treturn Array.isArray(errors)\n\t\t? errors.filter((error): error is ProviderGraphQLError => error != null && typeof error === 'object')\n\t\t: [];\n}\n\nexport function isAzureProviderId(\n\tproviderId: IntegrationIds,\n): providerId is GitCloudHostIntegrationId.AzureDevOps | GitSelfManagedHostIntegrationId.AzureDevOpsServer {\n\treturn (\n\t\tproviderId === GitCloudHostIntegrationId.AzureDevOps ||\n\t\tproviderId === GitSelfManagedHostIntegrationId.AzureDevOpsServer\n\t);\n}\n\nexport function isGitHubProviderId(\n\tproviderId: IntegrationIds,\n): providerId is GitCloudHostIntegrationId.GitHub | GitSelfManagedHostIntegrationId.CloudGitHubEnterprise {\n\treturn (\n\t\tproviderId === GitCloudHostIntegrationId.GitHub ||\n\t\tproviderId === GitSelfManagedHostIntegrationId.CloudGitHubEnterprise\n\t);\n}\n\n/**\n * GitHub's GraphQL schema declares coordinate variables (issue/PR number) as `Int!`, a 32-bit signed integer.\n * A number above this coerces with no `path` on the error, which fails every aliased target in the chunk rather\n * than just the one with the bad number — so the read layer refuses the whole call up front instead.\n */\nexport const githubGraphQLInt32Max = 2147483647;\n\nfunction isLinearRateLimitError(providerId: IntegrationIds, ex: unknown): boolean {\n\tif (providerId !== IssuesCloudHostIntegrationId.Linear) return false;\n\n\tconst body = (ex as { response?: { body?: unknown } }).response?.body;\n\treturn getProviderGraphQLErrors(body).some(error => error.extensions?.code === 'RATELIMITED');\n}\n\nexport function isProviderIssueNotFoundError(providerId: IntegrationIds, ex: unknown): boolean {\n\t// A rejected credential is not a missing issue, whatever status the server served it with.\n\tif (AnonymousResponseError.is(ex)) return false;\n\n\tif (providerId === IssuesCloudHostIntegrationId.Linear) {\n\t\treturn (\n\t\t\tex instanceof Error &&\n\t\t\t(linearIssueNotFoundMessage.test(ex.message) || linearIssueEntityNotFoundMessage.test(ex.message))\n\t\t);\n\t}\n\n\tconst status = (ex as { response?: { status?: unknown } }).response?.status;\n\treturn status === 404 || status === 410 || status === 422;\n}\n\nconst jiraMissingProjectMessage = /^The value '.*' does not exist for the field 'project'\\.?$/i;\n\n/**\n * Jira's refusal of a search scoped to a project it cannot find: `400` with\n * `{\"errorMessages\":[\"The value 'KPSC290' does not exist for the field 'project'.\"]}`, measured against Jira Data\n * Center 10.7.3 for a deleted project key and for a numeric id that names no project. Jira answers a project the\n * user lost browse permission on the same way, since it will not confirm a project the user cannot see.\n *\n * Only a `400` whose EVERY message is that one: a JQL that is wrong in some other way also answers `400`, and that\n * is a real failure rather than a project that is simply gone. Accepts the SDK's error or the `RequestClientError`\n * `throwProviderError` wraps it in, whose `original` keeps the response.\n */\nexport function isJiraMissingProjectError(ex: unknown): boolean {\n\treturn getJiraMissingProjectResponse(ex) != null;\n}\n\n/**\n * {@link isJiraMissingProjectError} for Jira Data Center, which serves a credential it rejects as the anonymous user:\n * the search then names a project the anonymous user cannot see and is refused with the very same `400`, measured\n * against Jira Data Center 10.7.3 with a revoked token. Only the user it names in `X-AUSERNAME` tells the two apart,\n * so a refusal that names no user, or `anonymous`, is a failure rather than a project that is gone.\n */\nexport function isJiraServerMissingProjectError(ex: unknown): boolean {\n\tconst user = getAuthenticatedUser(getJiraMissingProjectResponse(ex)?.headers);\n\treturn user != null && user !== 'anonymous';\n}\n\nfunction getJiraMissingProjectResponse(ex: unknown): { headers?: ResponseHeaders } | undefined {\n\tfor (const candidate of [ex, (ex as { original?: unknown } | undefined)?.original]) {\n\t\tconst response = (\n\t\t\tcandidate as { response?: { status?: unknown; body?: unknown; headers?: ResponseHeaders } } | undefined\n\t\t)?.response;\n\t\tif (response?.status !== 400) continue;\n\n\t\tconst messages = (response.body as { errorMessages?: unknown } | null | undefined)?.errorMessages;\n\t\tif (!Array.isArray(messages) || messages.length === 0) continue;\n\n\t\treturn messages.every(m => typeof m === 'string' && jiraMissingProjectMessage.test(m.trim()))\n\t\t\t? response\n\t\t\t: undefined;\n\t}\n\n\treturn undefined;\n}\n\nexport function throwProviderError(tokenWithInfo: TokenWithInfo, error: unknown): never {\n\tconst { accessToken: token, ...tokenInfo } = tokenWithInfo;\n\tconst providerId = tokenWithInfo.providerId;\n\tconst normalizedError = toError(error);\n\t// A page served with a 2xx is only known to mean a rejected credential on Azure DevOps, which answers one by\n\t// redirecting to its sign-in page (GKDEV-3617). Elsewhere it is far more likely a maintenance or WAF page in\n\t// front of a provider, and calling that an authentication failure is not free: `handleProviderException`\n\t// expires a valid cloud session on one, and counts it against the budget that disconnects the integration.\n\t// So Azure gets the specific diagnosis and every other provider gets the honest one — the response was not\n\t// data, which is a protocol failure.\n\tif (UnexpectedHtmlResponseError.is(error)) {\n\t\tthrow isAzureProviderId(providerId)\n\t\t\t? new AuthenticationError(tokenInfo, AuthenticationErrorReason.Unauthorized, normalizedError)\n\t\t\t: new RequestClientError(normalizedError);\n\t}\n\n\tif (AnonymousResponseError.is(error)) {\n\t\tthrow new AuthenticationError(tokenInfo, AuthenticationErrorReason.Unauthorized, normalizedError);\n\t}\n\n\t// Linear reports GraphQL throttling as HTTP 400, so its structured code must win over generic 4xx handling.\n\tif (isLinearRateLimitError(providerId, error)) {\n\t\tthrow toRateLimitError(normalizedError, token);\n\t}\n\n\tconst status = (error as { response?: { status?: unknown } }).response?.status;\n\tif (typeof status === 'number') {\n\t\tswitch (status) {\n\t\t\tcase 404:\n\t\t\tcase 410:\n\t\t\tcase 422:\n\t\t\t\tthrow new RequestNotFoundError(normalizedError);\n\t\t\tcase 429:\n\t\t\t\tthrow toRateLimitError(normalizedError, token);\n\t\t\tcase 401:\n\t\t\tcase 403:\n\t\t\t\t// Some hosts overload 403 for throttling; classify that before asking the user to reconnect.\n\t\t\t\tif (isRateLimitResponse({ status: status, message: normalizedError.message })) {\n\t\t\t\t\tthrow toRateLimitError(normalizedError, token);\n\t\t\t\t}\n\t\t\t\tthrow new AuthenticationError(\n\t\t\t\t\ttokenInfo,\n\t\t\t\t\tstatus === 401 ? AuthenticationErrorReason.Unauthorized : AuthenticationErrorReason.Forbidden,\n\t\t\t\t\tnormalizedError,\n\t\t\t\t);\n\t\t\tdefault:\n\t\t\t\tif (status >= 400 && status < 500) {\n\t\t\t\t\tthrow new RequestClientError(normalizedError);\n\t\t\t\t}\n\t\t}\n\t}\n\n\t// A `TypeError` the request function didn't throw was thrown while reading what it returned: a body that isn't the\n\t// shape the read expects (see `UnexpectedResponseError`). A transport failure that reached here some other way stays\n\t// one. Neither a session expiry nor a strike toward disconnecting, as before: nothing says the credential is at\n\t// fault, and the provider may answer properly next time. Logged here because a warning or a fan-out's scope failure\n\t// built from it carries only the readable message, and a fan-out never reaches `handleProviderException`'s log.\n\tif (error instanceof TypeError && !requestErrors.has(error) && !isProviderUnreachableError(error)) {\n\t\tconst unexpected = new UnexpectedResponseError(error);\n\t\tLogger.warn(`${tokenWithInfo.providerId}: ${String(unexpected)}`);\n\t\tthrow unexpected;\n\t}\n\n\tthrow error;\n}\n\n/** Extracts the useful provider prose from an already-parsed SDK response body. */\nexport function getProviderResponseBodyMessage(body: unknown): string | undefined {\n\tlet message: string | undefined;\n\tif (typeof body === 'string') {\n\t\tmessage = body;\n\t} else if (body != null && typeof body === 'object') {\n\t\tconst { message: direct, error } = body as { message?: unknown; error?: unknown };\n\t\tif (typeof direct === 'string') {\n\t\t\tmessage = direct;\n\t\t} else if (typeof error === 'string') {\n\t\t\tmessage = error;\n\t\t} else if (error != null && typeof error === 'object') {\n\t\t\tconst nested = (error as { message?: unknown }).message;\n\t\t\tif (typeof nested === 'string') {\n\t\t\t\tmessage = nested;\n\t\t\t}\n\t\t}\n\t\tif (message == null) {\n\t\t\tconst graphQLError = getProviderGraphQLErrors(body)[0];\n\t\t\tif (typeof graphQLError?.message === 'string') {\n\t\t\t\tmessage = graphQLError.message;\n\t\t\t} else if (typeof graphQLError?.extensions?.code === 'string') {\n\t\t\t\tmessage = graphQLError.extensions.code;\n\t\t\t}\n\t\t}\n\t}\n\n\tmessage = message?.trim();\n\treturn message ? message.slice(0, maxProviderErrorBodyLength) : undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"providerErrors.js","names":[],"sources":["../../../../../plus/integrations/src/providers/providerErrors.ts"],"sourcesContent":["import { Logger } from '@gitlens/utils/logger.js';\nimport type { TokenWithInfo } from '../authentication/models.js';\nimport type { IntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tIssuesCloudHostIntegrationId,\n} from '../constants.js';\nimport type { ResponseHeaders } from '../errors.js';\nimport {\n\tAuthenticationError,\n\tAuthenticationErrorReason,\n\tgetAuthenticatedUser,\n\tisProviderUnreachableError,\n\tisRateLimitResponse,\n\tRequestClientError,\n\tRequestNotFoundError,\n\ttoError,\n\ttoRateLimitError,\n} from '../errors.js';\n\nconst maxProviderErrorBodyLength = 500;\n\n/**\n * A 2xx response carrying an HTML page instead of data.\n *\n * Azure DevOps answers a rejected credential by redirecting to its sign-in page, which then returns\n * `203 text/html` rather than `401`. `response.ok` spans the whole 200-299 range, so that page used to be read as\n * a successful body and handed to provider code typed as the JSON it is not — surfacing as an opaque `TypeError`\n * deep inside a provider, or as nothing at all on a route that returns `void`, which made a bad credential look\n * like a successful write (GKDEV-3617).\n *\n * `provider-apis` 0.59.0 raises its own error for this, but only from the transport it wraps; this package passes\n * its own already-parsed request function, so the check has to live here too.\n *\n * The message carries no URL, and the attached response is stripped of its credential-bearing headers and cut to\n * a diagnostic prefix: an error is something a consumer may reasonably log or forward, and the response it is\n * built from is a sign-in page carrying a session cookie.\n */\nexport class UnexpectedHtmlResponseError extends Error {\n\tstatic is(ex: unknown): ex is UnexpectedHtmlResponseError {\n\t\treturn ex instanceof UnexpectedHtmlResponseError;\n\t}\n\n\treadonly response: SanitizedErrorResponse;\n\n\tconstructor(status: number, contentType: string, response: ProviderErrorResponse | undefined) {\n\t\tsuper(`(${status}) Expected data but the provider returned an HTML page (content-type: ${contentType})`);\n\n\t\tthis.response = sanitizeErrorResponse(status, response);\n\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\n/**\n * A provider read that failed on the data a request returned rather than on the request: `provider-apis` reads a\n * response body as the shape it expects without checking it, so a 2xx carrying anything else (a gateway's JSON\n * error, `{}`, `null`, a GraphQL answer with neither data nor errors) surfaces as a `TypeError` thrown deep inside the\n * read, such as `response.body.map is not a function` from Jira Cloud's site list or Trello's boards, or\n * `Cannot read properties of undefined (reading 'id')` from Linear's organization. See {@link throwProviderError}.\n *\n * The message is the readable one a warning carries. The `TypeError` is kept as `original`, and a log line, which\n * prints the error through `toString()`, names it too.\n */\nexport class UnexpectedResponseError extends Error {\n\tstatic is(ex: unknown): ex is UnexpectedResponseError {\n\t\treturn ex instanceof UnexpectedResponseError;\n\t}\n\n\tconstructor(readonly original: Error) {\n\t\tsuper('Expected data but the provider returned an unexpected response');\n\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n\n\toverride toString(): string {\n\t\treturn `${super.toString()} (${String(this.original)})`;\n\t}\n}\n\n/** What the request function `ProvidersApi` hands `provider-apis` threw itself: see {@link markRequestError}. */\nconst requestErrors = new WeakSet<object>();\n\n/**\n * Records an error the request function threw itself (a transport failure, or a response it refused), so\n * {@link throwProviderError} never reads it as data a read could not handle, whatever the host's fetch throws.\n * Returns `ex`, to rethrow.\n */\nexport function markRequestError(ex: unknown): unknown {\n\tif (ex != null && typeof ex === 'object') {\n\t\trequestErrors.add(ex);\n\t}\n\treturn ex;\n}\n\n/**\n * A response that names the anonymous user (`X-AUSERNAME: anonymous`) to a request that carried a credential.\n *\n * Jira Data Center serves a token it rejects as the anonymous user rather than refusing it: measured against Jira\n * Data Center 10.7.3 with a revoked token, `/rest/api/2/project` answers `200 []` and a project search answers the\n * same `400` it gives a deleted project. Read as data, that is an account with no projects, and a selected project\n * looks deleted. The server only names `anonymous` when the credential did not authenticate, so the response is the\n * credential's rejection whatever its status, except a 401, which already fails as one, and a 429 or a 5xx, which\n * prove nothing about the credential.\n *\n * Its response keeps only the status and headers, stripped like {@link UnexpectedHtmlResponseError}'s.\n */\nexport class AnonymousResponseError extends Error {\n\tstatic is(ex: unknown): ex is AnonymousResponseError {\n\t\treturn ex instanceof AnonymousResponseError;\n\t}\n\n\treadonly response: SanitizedErrorResponse;\n\n\tconstructor(status: number, response: ProviderErrorResponse | undefined) {\n\t\tsuper(`(${status}) The provider served a request sent with a credential as the anonymous user`);\n\n\t\tthis.response = sanitizeErrorResponse(status, response);\n\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\ntype ProviderErrorResponse = { body?: unknown; headers?: Record<string, string>; status?: number };\ntype SanitizedErrorResponse = { body?: string; headers: Record<string, string>; status: number };\n\n/**\n * Response headers a rejected-credential page carries that must not ride along on an error: the session cookie it\n * sets, the challenges whose base64 payload decodes to the tenant or the internal domain, and the request-side\n * names a substituted fetch can echo back onto a response.\n */\nconst unloggableResponseHeaders = new Set([\n\t'authentication-info',\n\t'authorization',\n\t'cookie',\n\t'proxy-authenticate',\n\t'proxy-authentication-info',\n\t'proxy-authorization',\n\t'set-cookie',\n\t'www-authenticate',\n\t'x-api-key',\n\t'x-auth-token',\n]);\n\n/** Strips the credential-bearing headers from a response and cuts its body to a diagnostic prefix. */\nfunction sanitizeErrorResponse(status: number, response: ProviderErrorResponse | undefined): SanitizedErrorResponse {\n\tconst headers: Record<string, string> = {};\n\tfor (const [name, value] of Object.entries(response?.headers ?? {})) {\n\t\tif (unloggableResponseHeaders.has(name.toLowerCase())) continue;\n\n\t\theaders[name] = value;\n\t}\n\n\tconst body = response?.body;\n\treturn {\n\t\t// A sign-in page runs to tens of kilobytes and none of it is a diagnostic; keep only what identifies it.\n\t\t...(typeof body === 'string' ? { body: body.slice(0, maxProviderErrorBodyLength) } : {}),\n\t\theaders: headers,\n\t\tstatus: response?.status ?? status,\n\t};\n}\n\nconst linearIssueNotFoundMessage = /^Linear issue not found: .+$/i;\n\n/**\n * What Linear actually answers for an identifier it can't resolve: HTTP 200 with the single GraphQL error\n * `Entity not found: Issue` (`INPUT_ERROR`), which provider-apis rethrows as `Linear GraphQL errors: <messages>`, the\n * response not attached. Only that one message proves the miss; any other error, or another alongside it, is a\n * failure.\n */\nconst linearIssueEntityNotFoundMessage = /^Linear GraphQL errors: Entity not found: Issue$/;\n\ntype ProviderGraphQLError = {\n\tmessage?: unknown;\n\textensions?: { code?: unknown };\n};\n\nfunction getProviderGraphQLErrors(body: unknown): ProviderGraphQLError[] {\n\tif (body == null || typeof body !== 'object') return [];\n\n\tconst errors = (body as { errors?: unknown }).errors;\n\treturn Array.isArray(errors)\n\t\t? errors.filter((error): error is ProviderGraphQLError => error != null && typeof error === 'object')\n\t\t: [];\n}\n\nexport function isAzureProviderId(\n\tproviderId: IntegrationIds,\n): providerId is GitCloudHostIntegrationId.AzureDevOps | GitSelfManagedHostIntegrationId.AzureDevOpsServer {\n\treturn (\n\t\tproviderId === GitCloudHostIntegrationId.AzureDevOps ||\n\t\tproviderId === GitSelfManagedHostIntegrationId.AzureDevOpsServer\n\t);\n}\n\nexport function isGitHubProviderId(\n\tproviderId: IntegrationIds,\n): providerId is GitCloudHostIntegrationId.GitHub | GitSelfManagedHostIntegrationId.CloudGitHubEnterprise {\n\treturn (\n\t\tproviderId === GitCloudHostIntegrationId.GitHub ||\n\t\tproviderId === GitSelfManagedHostIntegrationId.CloudGitHubEnterprise\n\t);\n}\n\n/**\n * GitHub's GraphQL schema declares coordinate variables (issue/PR number) as `Int!`, a 32-bit signed integer.\n * A number above this coerces with no `path` on the error, which fails every aliased target in the chunk rather\n * than just the one with the bad number — so the read layer refuses the whole call up front instead.\n */\nexport const githubGraphQLInt32Max = 2147483647;\n\nfunction isLinearRateLimitError(providerId: IntegrationIds, ex: unknown): boolean {\n\tif (providerId !== IssuesCloudHostIntegrationId.Linear) return false;\n\n\treturn isLinearRateLimitedBody((ex as { response?: { body?: unknown } }).response?.body);\n}\n\n/** Whether a Linear GraphQL response body reports throttling. */\nexport function isLinearRateLimitedBody(body: unknown): boolean {\n\treturn getProviderGraphQLErrors(body).some(error => error.extensions?.code === 'RATELIMITED');\n}\n\nexport function isProviderIssueNotFoundError(providerId: IntegrationIds, ex: unknown): boolean {\n\t// A rejected credential is not a missing issue, whatever status the server served it with.\n\tif (AnonymousResponseError.is(ex)) return false;\n\n\tif (providerId === IssuesCloudHostIntegrationId.Linear) {\n\t\treturn (\n\t\t\tex instanceof Error &&\n\t\t\t(linearIssueNotFoundMessage.test(ex.message) || linearIssueEntityNotFoundMessage.test(ex.message))\n\t\t);\n\t}\n\n\tconst status = (ex as { response?: { status?: unknown } }).response?.status;\n\treturn status === 404 || status === 410 || status === 422;\n}\n\nconst jiraMissingProjectMessage = /^The value '.*' does not exist for the field 'project'\\.?$/i;\n\n/**\n * Jira's refusal of a search scoped to a project it cannot find: `400` with\n * `{\"errorMessages\":[\"The value 'KPSC290' does not exist for the field 'project'.\"]}`, measured against Jira Data\n * Center 10.7.3 for a deleted project key and for a numeric id that names no project. Jira answers a project the\n * user lost browse permission on the same way, since it will not confirm a project the user cannot see.\n *\n * Only a `400` whose EVERY message is that one: a JQL that is wrong in some other way also answers `400`, and that\n * is a real failure rather than a project that is simply gone. Accepts the SDK's error or the `RequestClientError`\n * `throwProviderError` wraps it in, whose `original` keeps the response.\n */\nexport function isJiraMissingProjectError(ex: unknown): boolean {\n\treturn getJiraMissingProjectResponse(ex) != null;\n}\n\n/**\n * {@link isJiraMissingProjectError} for Jira Data Center, which serves a credential it rejects as the anonymous user:\n * the search then names a project the anonymous user cannot see and is refused with the very same `400`, measured\n * against Jira Data Center 10.7.3 with a revoked token. Only the user it names in `X-AUSERNAME` tells the two apart,\n * so a refusal that names no user, or `anonymous`, is a failure rather than a project that is gone.\n */\nexport function isJiraServerMissingProjectError(ex: unknown): boolean {\n\tconst user = getAuthenticatedUser(getJiraMissingProjectResponse(ex)?.headers);\n\treturn user != null && user !== 'anonymous';\n}\n\nfunction getJiraMissingProjectResponse(ex: unknown): { headers?: ResponseHeaders } | undefined {\n\tfor (const candidate of [ex, (ex as { original?: unknown } | undefined)?.original]) {\n\t\tconst response = (\n\t\t\tcandidate as { response?: { status?: unknown; body?: unknown; headers?: ResponseHeaders } } | undefined\n\t\t)?.response;\n\t\tif (response?.status !== 400) continue;\n\n\t\tconst messages = (response.body as { errorMessages?: unknown } | null | undefined)?.errorMessages;\n\t\tif (!Array.isArray(messages) || messages.length === 0) continue;\n\n\t\treturn messages.every(m => typeof m === 'string' && jiraMissingProjectMessage.test(m.trim()))\n\t\t\t? response\n\t\t\t: undefined;\n\t}\n\n\treturn undefined;\n}\n\nexport function throwProviderError(tokenWithInfo: TokenWithInfo, error: unknown): never {\n\tconst { accessToken: token, ...tokenInfo } = tokenWithInfo;\n\tconst providerId = tokenWithInfo.providerId;\n\tconst normalizedError = toError(error);\n\t// A page served with a 2xx is only known to mean a rejected credential on Azure DevOps, which answers one by\n\t// redirecting to its sign-in page (GKDEV-3617). Elsewhere it is far more likely a maintenance or WAF page in\n\t// front of a provider, and calling that an authentication failure is not free: `handleProviderException`\n\t// expires a valid cloud session on one, and counts it against the budget that disconnects the integration.\n\t// So Azure gets the specific diagnosis and every other provider gets the honest one — the response was not\n\t// data, which is a protocol failure.\n\tif (UnexpectedHtmlResponseError.is(error)) {\n\t\tthrow isAzureProviderId(providerId)\n\t\t\t? new AuthenticationError(tokenInfo, AuthenticationErrorReason.Unauthorized, normalizedError)\n\t\t\t: new RequestClientError(normalizedError);\n\t}\n\n\tif (AnonymousResponseError.is(error)) {\n\t\tthrow new AuthenticationError(tokenInfo, AuthenticationErrorReason.Unauthorized, normalizedError);\n\t}\n\n\t// Linear reports GraphQL throttling as HTTP 400, so its structured code must win over generic 4xx handling.\n\tif (isLinearRateLimitError(providerId, error)) {\n\t\tthrow toRateLimitError(normalizedError, token);\n\t}\n\n\tconst status = (error as { response?: { status?: unknown } }).response?.status;\n\tif (typeof status === 'number') {\n\t\tswitch (status) {\n\t\t\tcase 404:\n\t\t\tcase 410:\n\t\t\tcase 422:\n\t\t\t\tthrow new RequestNotFoundError(normalizedError);\n\t\t\tcase 429:\n\t\t\t\tthrow toRateLimitError(normalizedError, token);\n\t\t\tcase 401:\n\t\t\tcase 403:\n\t\t\t\t// Some hosts overload 403 for throttling; classify that before asking the user to reconnect.\n\t\t\t\tif (isRateLimitResponse({ status: status, message: normalizedError.message })) {\n\t\t\t\t\tthrow toRateLimitError(normalizedError, token);\n\t\t\t\t}\n\t\t\t\tthrow new AuthenticationError(\n\t\t\t\t\ttokenInfo,\n\t\t\t\t\tstatus === 401 ? AuthenticationErrorReason.Unauthorized : AuthenticationErrorReason.Forbidden,\n\t\t\t\t\tnormalizedError,\n\t\t\t\t);\n\t\t\tdefault:\n\t\t\t\tif (status >= 400 && status < 500) {\n\t\t\t\t\tthrow new RequestClientError(normalizedError);\n\t\t\t\t}\n\t\t}\n\t}\n\n\t// A `TypeError` the request function didn't throw was thrown while reading what it returned: a body that isn't the\n\t// shape the read expects (see `UnexpectedResponseError`). A transport failure that reached here some other way stays\n\t// one. Neither a session expiry nor a strike toward disconnecting, as before: nothing says the credential is at\n\t// fault, and the provider may answer properly next time. Logged here because a warning or a fan-out's scope failure\n\t// built from it carries only the readable message, and a fan-out never reaches `handleProviderException`'s log.\n\tif (error instanceof TypeError && !requestErrors.has(error) && !isProviderUnreachableError(error)) {\n\t\tconst unexpected = new UnexpectedResponseError(error);\n\t\tLogger.warn(`${tokenWithInfo.providerId}: ${String(unexpected)}`);\n\t\tthrow unexpected;\n\t}\n\n\tthrow error;\n}\n\n/** Extracts the useful provider prose from an already-parsed SDK response body. */\nexport function getProviderResponseBodyMessage(body: unknown): string | undefined {\n\tlet message: string | undefined;\n\tif (typeof body === 'string') {\n\t\tmessage = body;\n\t} else if (body != null && typeof body === 'object') {\n\t\tconst { message: direct, error } = body as { message?: unknown; error?: unknown };\n\t\tif (typeof direct === 'string') {\n\t\t\tmessage = direct;\n\t\t} else if (typeof error === 'string') {\n\t\t\tmessage = error;\n\t\t} else if (error != null && typeof error === 'object') {\n\t\t\tconst nested = (error as { message?: unknown }).message;\n\t\t\tif (typeof nested === 'string') {\n\t\t\t\tmessage = nested;\n\t\t\t}\n\t\t}\n\t\tif (message == null) {\n\t\t\tconst graphQLError = getProviderGraphQLErrors(body)[0];\n\t\t\tif (typeof graphQLError?.message === 'string') {\n\t\t\t\tmessage = graphQLError.message;\n\t\t\t} else if (typeof graphQLError?.extensions?.code === 'string') {\n\t\t\t\tmessage = graphQLError.extensions.code;\n\t\t\t}\n\t\t}\n\t}\n\n\tmessage = message?.trim();\n\treturn message ? message.slice(0, maxProviderErrorBodyLength) : undefined;\n}\n"],"mappings":"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"}
@@ -30,7 +30,7 @@ export declare class TrelloIntegration extends IssuesIntegration<IssuesCloudHost
30
30
  protected getProviderCurrentAccount(session: ProviderAuthenticationSession): Promise<Account | undefined>;
31
31
  protected getProviderUncachedAccountForResource(session: ProviderAuthenticationSession, _resource: ResourceDescriptor): Promise<Account | undefined>;
32
32
  protected getProviderAccountForResource(session: ProviderAuthenticationSession, _resource: ResourceDescriptor): Promise<Account | undefined>;
33
- invalidateDiscoveryCaches(): void;
33
+ protected clearDiscoveryCaches(): void;
34
34
  private readonly _recentAccounts;
35
35
  private fetchAccount;
36
36
  protected getProviderResourcesForUser(session: ProviderAuthenticationSession): Promise<TrelloBoardDescriptor[] | undefined>;
@@ -56,7 +56,7 @@ var TrelloIntegration = class extends IssuesIntegration {
56
56
  const appKey = this.requireAppKey(session);
57
57
  return this._recentAccounts.getOrCreate(session.accessToken, () => this.fetchAccount(session, appKey), { evictWhen: (account) => account == null });
58
58
  }
59
- invalidateDiscoveryCaches() {
59
+ clearDiscoveryCaches() {
60
60
  this._recentAccounts.clear();
61
61
  }
62
62
  _recentAccounts = new PromiseCache({
@@ -1 +1 @@
1
- {"version":3,"file":"trello.js","names":[],"sources":["../../../../../plus/integrations/src/providers/trello.ts"],"sourcesContent":["import type { Account } from '@gitlens/git/models/author.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest, IssueOrPullRequestType } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor, ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { isIssueResourceDescriptor } from '@gitlens/git/utils/resourceDescriptor.utils.js';\nimport { PromiseCache } from '@gitlens/utils/promiseCache.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport { IntegrationReadUnavailableError } from '../errors.js';\nimport type { IssuesForProjectOptions, ProjectIssuesDrain } from '../models/issueReads.js';\nimport { IssuesIntegration } from '../models/issuesIntegration.js';\nimport { fromProviderIssue, providersMetadata, toIssueShape } from './models.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Trello];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\n/** Long enough to span one tracker read, short enough that a profile change shows on the next. */\nconst recentAccountTTL = 60 * 1000;\n\n/** A Trello board, surfaced as both a \"resource\" (org analogue) and a \"project\" for issue reads. */\nexport interface TrelloBoardDescriptor extends IssueResourceDescriptor {}\n\nexport class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Trello> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\n\toverride get id(): IssuesCloudHostIntegrationId.Trello {\n\t\treturn IssuesCloudHostIntegrationId.Trello;\n\t}\n\tprotected override get key(): 'trello' {\n\t\treturn 'trello';\n\t}\n\toverride get name(): string {\n\t\treturn metadata.name;\n\t}\n\toverride get domain(): string {\n\t\treturn metadata.domain;\n\t}\n\n\t/** The Trello client requires the app key paired with the token; a session without one can't read. */\n\tprivate appKeyFor(session: ProviderAuthenticationSession): string | undefined {\n\t\treturn session.appKey;\n\t}\n\n\t/**\n\t * Resolves the app key or throws — a session that authenticated but carries no app key can't read Trello,\n\t * and must be surfaced as a broken read (warning + fetchFailed) rather than an empty account.\n\t */\n\tprivate requireAppKey(session: ProviderAuthenticationSession): string {\n\t\tconst appKey = this.appKeyFor(session);\n\t\tif (appKey == null) {\n\t\t\tthrow new IntegrationReadUnavailableError(metadata.name, 'missing app key (session has no appKey)');\n\t\t}\n\t\treturn appKey;\n\t}\n\n\t/**\n\t * Always asks Trello: the host's current-account cache decides when this runs. The member lookup is\n\t * account-wide, so the answer is lent briefly to the per-board lookup a tracker read makes right after it.\n\t */\n\tprotected override async getProviderCurrentAccount(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<Account | undefined> {\n\t\tconst account = await this.fetchAccount(session);\n\t\tif (account != null) {\n\t\t\tthis._recentAccounts.set(session.accessToken, Promise.resolve(account));\n\t\t}\n\t\treturn account;\n\t}\n\n\tprotected override getProviderUncachedAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\treturn this.fetchAccount(session, this.requireAppKey(session));\n\t}\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst appKey = this.requireAppKey(session);\n\t\treturn this._recentAccounts.getOrCreate(session.accessToken, () => this.fetchAccount(session, appKey), {\n\t\t\tevictWhen: account => account == null,\n\t\t});\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._recentAccounts.clear();\n\t}\n\n\tprivate readonly _recentAccounts = new PromiseCache<string, Account | undefined>({\n\t\tcapacity: 10,\n\t\tcreateTTL: recentAccountTTL,\n\t});\n\n\tprivate async fetchAccount(\n\t\tsession: ProviderAuthenticationSession,\n\t\tappKey: string = this.requireAppKey(session),\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getTrelloCurrentUser(toTokenWithInfo(this.id, session), appKey);\n\t\tif (user == null) return undefined;\n\n\t\treturn {\n\t\t\tprovider: this,\n\t\t\tid: user.id,\n\t\t\tname: user.name,\n\t\t\tusername: user.username,\n\t\t\temail: user.email,\n\t\t\tavatarUrl: user.avatarUrl ?? undefined,\n\t\t};\n\t}\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<TrelloBoardDescriptor[] | undefined> {\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst boards = await api.getTrelloBoardsForCurrentUser(toTokenWithInfo(this.id, session), appKey);\n\t\treturn boards?.map(b => ({ key: b.id, id: b.id, name: b.name }));\n\t}\n\n\tprotected override getProviderProjectsForResources(\n\t\t_session: ProviderAuthenticationSession,\n\t\tresources: ResourceDescriptor[],\n\t): Promise<TrelloBoardDescriptor[] | undefined> {\n\t\t// Trello boards are both the resource and the project scope, so projects are the boards themselves.\n\t\treturn Promise.resolve(\n\t\t\tresources.filter(isIssueResourceDescriptor).map(r => ({ key: r.key, id: r.id, name: r.name })),\n\t\t);\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\t// A non-issue descriptor genuinely has nothing to read (empty), but a missing app key is a broken read.\n\t\tif (!isIssueResourceDescriptor(project)) return undefined;\n\n\t\t// The board read always excludes archived cards, and a card has no other state, so only open cards can be\n\t\t// read. Refused rather than ignored: answering `closed` with the open cards would publish them as closed.\n\t\tif (options?.state != null && options.state !== 'open') {\n\t\t\tthrow new IntegrationReadUnavailableError(metadata.name, `issue state '${options.state}' is not supported`);\n\t\t}\n\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\t// Enrich issues with their board list names (card status), mirroring gitkraken.dev's Trello reads.\n\t\tconst lists = await api.getTrelloListsForBoard(tokenWithInfo, appKey, project.id);\n\t\tconst trelloBoardListsById = lists?.reduce<Record<string, { name: string }>>((acc, list) => {\n\t\t\tacc[list.id] = { name: list.name };\n\t\t\treturn acc;\n\t\t}, {});\n\t\tconst boardProject = {\n\t\t\tid: project.id,\n\t\t\tname: project.name,\n\t\t\tresourceId: project.id,\n\t\t\tresourceName: project.name,\n\t\t};\n\n\t\tconst result = await api.getTrelloIssuesForBoard(tokenWithInfo, appKey, project.id, {\n\t\t\tassigneeLogins: options?.user != null ? [options.user] : undefined,\n\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t\tsort: options?.sort,\n\t\t});\n\n\t\tconst values = result.values.flatMap(issue => {\n\t\t\tconst mapped = toIssueShape(issue, this, { projection: 'project' });\n\t\t\treturn mapped == null ? [] : [{ ...mapped, project: boardProject } satisfies IssueShape];\n\t\t});\n\n\t\t// Trello's search caps results and reports the cap through `metadata.completeness` (partial/unknown),\n\t\t// never a cursor. Surface that as terminal truncation; there is no next page to fetch, so retrying the\n\t\t// same read cannot recover the omitted cards (D11).\n\t\tconst truncated = result.metadata != null && result.metadata.completeness !== 'complete';\n\t\treturn truncated\n\t\t\t? { values: values, truncated: true, recovery: 'none', metadata: result.metadata }\n\t\t\t: { values: values, truncated: false, metadata: result.metadata };\n\t}\n\n\tprotected override searchProviderMyIssues(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resources?: ResourceDescriptor[],\n\t\t_cancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\t// Trello has no cross-board \"issues assigned to me\" endpoint; issues are read per board via\n\t\t// getIssuesForProject. Callers that need them enumerate boards first.\n\t\treturn Promise.resolve(undefined);\n\t}\n\n\tprotected override getProviderLinkedIssueOrPullRequest(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t\t_id: { id: string; key: string },\n\t\t_type: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\treturn Promise.resolve(undefined);\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tif (!isIssueResourceDescriptor(resource)) return undefined;\n\n\t\tconst appKey = this.requireAppKey(session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst lists = await api.getTrelloListsForBoard(tokenWithInfo, appKey, resource.id);\n\t\tconst trelloBoardListsById = lists?.reduce<Record<string, { name: string }>>((acc, list) => {\n\t\t\tacc[list.id] = { name: list.name };\n\t\t\treturn acc;\n\t\t}, {});\n\n\t\t// Branch-association round-trips use the encoded entity identifier's stable Trello card id. Resolve that\n\t\t// directly so large boards don't require a capped whole-board scan; keep a numeric idShort fallback so\n\t\t// legacy/manual callers that still pass the board-local display number continue to resolve.\n\t\tlet issue = await api.getTrelloCard(tokenWithInfo, appKey, id, {\n\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t});\n\t\tif (issue == null && /^[0-9]+$/.test(id)) {\n\t\t\tissue = (\n\t\t\t\tawait api.getTrelloIssuesForBoard(tokenWithInfo, appKey, resource.id, {\n\t\t\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t\t\t})\n\t\t\t).values.find(issue => issue.number === id);\n\t\t}\n\n\t\treturn issue != null\n\t\t\t? fromProviderIssue(issue, this, {\n\t\t\t\t\tprojection: 'point',\n\t\t\t\t\tproject: {\n\t\t\t\t\t\tid: resource.id,\n\t\t\t\t\t\tname: resource.name,\n\t\t\t\t\t\tresourceId: resource.id,\n\t\t\t\t\t\tresourceName: resource.name,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t: undefined;\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"trello.js","names":[],"sources":["../../../../../plus/integrations/src/providers/trello.ts"],"sourcesContent":["import type { Account } from '@gitlens/git/models/author.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest, IssueOrPullRequestType } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor, ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { isIssueResourceDescriptor } from '@gitlens/git/utils/resourceDescriptor.utils.js';\nimport { PromiseCache } from '@gitlens/utils/promiseCache.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport { IntegrationReadUnavailableError } from '../errors.js';\nimport type { IssuesForProjectOptions, ProjectIssuesDrain } from '../models/issueReads.js';\nimport { IssuesIntegration } from '../models/issuesIntegration.js';\nimport { fromProviderIssue, providersMetadata, toIssueShape } from './models.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Trello];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\n/** Long enough to span one tracker read, short enough that a profile change shows on the next. */\nconst recentAccountTTL = 60 * 1000;\n\n/** A Trello board, surfaced as both a \"resource\" (org analogue) and a \"project\" for issue reads. */\nexport interface TrelloBoardDescriptor extends IssueResourceDescriptor {}\n\nexport class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Trello> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\n\toverride get id(): IssuesCloudHostIntegrationId.Trello {\n\t\treturn IssuesCloudHostIntegrationId.Trello;\n\t}\n\tprotected override get key(): 'trello' {\n\t\treturn 'trello';\n\t}\n\toverride get name(): string {\n\t\treturn metadata.name;\n\t}\n\toverride get domain(): string {\n\t\treturn metadata.domain;\n\t}\n\n\t/** The Trello client requires the app key paired with the token; a session without one can't read. */\n\tprivate appKeyFor(session: ProviderAuthenticationSession): string | undefined {\n\t\treturn session.appKey;\n\t}\n\n\t/**\n\t * Resolves the app key or throws — a session that authenticated but carries no app key can't read Trello,\n\t * and must be surfaced as a broken read (warning + fetchFailed) rather than an empty account.\n\t */\n\tprivate requireAppKey(session: ProviderAuthenticationSession): string {\n\t\tconst appKey = this.appKeyFor(session);\n\t\tif (appKey == null) {\n\t\t\tthrow new IntegrationReadUnavailableError(metadata.name, 'missing app key (session has no appKey)');\n\t\t}\n\t\treturn appKey;\n\t}\n\n\t/**\n\t * Always asks Trello: the host's current-account cache decides when this runs. The member lookup is\n\t * account-wide, so the answer is lent briefly to the per-board lookup a tracker read makes right after it.\n\t */\n\tprotected override async getProviderCurrentAccount(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<Account | undefined> {\n\t\tconst account = await this.fetchAccount(session);\n\t\tif (account != null) {\n\t\t\tthis._recentAccounts.set(session.accessToken, Promise.resolve(account));\n\t\t}\n\t\treturn account;\n\t}\n\n\tprotected override getProviderUncachedAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\treturn this.fetchAccount(session, this.requireAppKey(session));\n\t}\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst appKey = this.requireAppKey(session);\n\t\treturn this._recentAccounts.getOrCreate(session.accessToken, () => this.fetchAccount(session, appKey), {\n\t\t\tevictWhen: account => account == null,\n\t\t});\n\t}\n\n\tprotected override clearDiscoveryCaches(): void {\n\t\tthis._recentAccounts.clear();\n\t}\n\n\tprivate readonly _recentAccounts = new PromiseCache<string, Account | undefined>({\n\t\tcapacity: 10,\n\t\tcreateTTL: recentAccountTTL,\n\t});\n\n\tprivate async fetchAccount(\n\t\tsession: ProviderAuthenticationSession,\n\t\tappKey: string = this.requireAppKey(session),\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getTrelloCurrentUser(toTokenWithInfo(this.id, session), appKey);\n\t\tif (user == null) return undefined;\n\n\t\treturn {\n\t\t\tprovider: this,\n\t\t\tid: user.id,\n\t\t\tname: user.name,\n\t\t\tusername: user.username,\n\t\t\temail: user.email,\n\t\t\tavatarUrl: user.avatarUrl ?? undefined,\n\t\t};\n\t}\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<TrelloBoardDescriptor[] | undefined> {\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst boards = await api.getTrelloBoardsForCurrentUser(toTokenWithInfo(this.id, session), appKey);\n\t\treturn boards?.map(b => ({ key: b.id, id: b.id, name: b.name }));\n\t}\n\n\tprotected override getProviderProjectsForResources(\n\t\t_session: ProviderAuthenticationSession,\n\t\tresources: ResourceDescriptor[],\n\t): Promise<TrelloBoardDescriptor[] | undefined> {\n\t\t// Trello boards are both the resource and the project scope, so projects are the boards themselves.\n\t\treturn Promise.resolve(\n\t\t\tresources.filter(isIssueResourceDescriptor).map(r => ({ key: r.key, id: r.id, name: r.name })),\n\t\t);\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\t// A non-issue descriptor genuinely has nothing to read (empty), but a missing app key is a broken read.\n\t\tif (!isIssueResourceDescriptor(project)) return undefined;\n\n\t\t// The board read always excludes archived cards, and a card has no other state, so only open cards can be\n\t\t// read. Refused rather than ignored: answering `closed` with the open cards would publish them as closed.\n\t\tif (options?.state != null && options.state !== 'open') {\n\t\t\tthrow new IntegrationReadUnavailableError(metadata.name, `issue state '${options.state}' is not supported`);\n\t\t}\n\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\t// Enrich issues with their board list names (card status), mirroring gitkraken.dev's Trello reads.\n\t\tconst lists = await api.getTrelloListsForBoard(tokenWithInfo, appKey, project.id);\n\t\tconst trelloBoardListsById = lists?.reduce<Record<string, { name: string }>>((acc, list) => {\n\t\t\tacc[list.id] = { name: list.name };\n\t\t\treturn acc;\n\t\t}, {});\n\t\tconst boardProject = {\n\t\t\tid: project.id,\n\t\t\tname: project.name,\n\t\t\tresourceId: project.id,\n\t\t\tresourceName: project.name,\n\t\t};\n\n\t\tconst result = await api.getTrelloIssuesForBoard(tokenWithInfo, appKey, project.id, {\n\t\t\tassigneeLogins: options?.user != null ? [options.user] : undefined,\n\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t\tsort: options?.sort,\n\t\t});\n\n\t\tconst values = result.values.flatMap(issue => {\n\t\t\tconst mapped = toIssueShape(issue, this, { projection: 'project' });\n\t\t\treturn mapped == null ? [] : [{ ...mapped, project: boardProject } satisfies IssueShape];\n\t\t});\n\n\t\t// Trello's search caps results and reports the cap through `metadata.completeness` (partial/unknown),\n\t\t// never a cursor. Surface that as terminal truncation; there is no next page to fetch, so retrying the\n\t\t// same read cannot recover the omitted cards (D11).\n\t\tconst truncated = result.metadata != null && result.metadata.completeness !== 'complete';\n\t\treturn truncated\n\t\t\t? { values: values, truncated: true, recovery: 'none', metadata: result.metadata }\n\t\t\t: { values: values, truncated: false, metadata: result.metadata };\n\t}\n\n\tprotected override searchProviderMyIssues(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resources?: ResourceDescriptor[],\n\t\t_cancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\t// Trello has no cross-board \"issues assigned to me\" endpoint; issues are read per board via\n\t\t// getIssuesForProject. Callers that need them enumerate boards first.\n\t\treturn Promise.resolve(undefined);\n\t}\n\n\tprotected override getProviderLinkedIssueOrPullRequest(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t\t_id: { id: string; key: string },\n\t\t_type: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\treturn Promise.resolve(undefined);\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tif (!isIssueResourceDescriptor(resource)) return undefined;\n\n\t\tconst appKey = this.requireAppKey(session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst lists = await api.getTrelloListsForBoard(tokenWithInfo, appKey, resource.id);\n\t\tconst trelloBoardListsById = lists?.reduce<Record<string, { name: string }>>((acc, list) => {\n\t\t\tacc[list.id] = { name: list.name };\n\t\t\treturn acc;\n\t\t}, {});\n\n\t\t// Branch-association round-trips use the encoded entity identifier's stable Trello card id. Resolve that\n\t\t// directly so large boards don't require a capped whole-board scan; keep a numeric idShort fallback so\n\t\t// legacy/manual callers that still pass the board-local display number continue to resolve.\n\t\tlet issue = await api.getTrelloCard(tokenWithInfo, appKey, id, {\n\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t});\n\t\tif (issue == null && /^[0-9]+$/.test(id)) {\n\t\t\tissue = (\n\t\t\t\tawait api.getTrelloIssuesForBoard(tokenWithInfo, appKey, resource.id, {\n\t\t\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t\t\t})\n\t\t\t).values.find(issue => issue.number === id);\n\t\t}\n\n\t\treturn issue != null\n\t\t\t? fromProviderIssue(issue, this, {\n\t\t\t\t\tprojection: 'point',\n\t\t\t\t\tproject: {\n\t\t\t\t\t\tid: resource.id,\n\t\t\t\t\t\tname: resource.name,\n\t\t\t\t\t\tresourceId: resource.id,\n\t\t\t\t\t\tresourceName: resource.name,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t: undefined;\n\t}\n}\n"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { ResourceDescriptor } from "../../../../git/models/resourceDescriptor.js";
2
+ import { AutolinkReference } from "../../../../git/models/autolink.js";
3
+ //#region ../plus/integrations/src/providers/utils/issueAutolinks.d.ts
4
+ export declare function createIssueAutolinkReferences(key: string, url: string, resource: ResourceDescriptor & {
5
+ name: string;
6
+ }): AutolinkReference[];
7
+ //#endregion
8
+ //# sourceMappingURL=issueAutolinks.d.ts.map
@@ -0,0 +1,20 @@
1
+ import * as l10n from "@vscode/l10n";
2
+ //#region ../plus/integrations/src/providers/utils/issueAutolinks.ts
3
+ function createIssueAutolinkReferences(key, url, resource) {
4
+ const dashedPrefix = `${key}-`;
5
+ return [dashedPrefix, `${key}_`].map((prefix) => ({
6
+ prefix,
7
+ url,
8
+ alphanumeric: false,
9
+ ignoreCase: false,
10
+ referenceType: prefix === dashedPrefix ? void 0 : "branch",
11
+ title: l10n.t("Open Issue {0} on {1}", `${dashedPrefix}<num>`, resource.name),
12
+ type: "issue",
13
+ description: l10n.t("{0} Issue {1}", resource.name, `${dashedPrefix}<num>`),
14
+ descriptor: { ...resource }
15
+ }));
16
+ }
17
+ //#endregion
18
+ export { createIssueAutolinkReferences };
19
+
20
+ //# sourceMappingURL=issueAutolinks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issueAutolinks.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/utils/issueAutolinks.ts"],"sourcesContent":["import * as l10n from '@vscode/l10n';\nimport type { AutolinkReference } from '@gitlens/git/models/autolink.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\n\nexport function createIssueAutolinkReferences(\n\tkey: string,\n\turl: string,\n\tresource: ResourceDescriptor & { name: string },\n): AutolinkReference[] {\n\tconst dashedPrefix = `${key}-`;\n\treturn [dashedPrefix, `${key}_`].map<AutolinkReference>(prefix => ({\n\t\tprefix: prefix,\n\t\turl: url,\n\t\talphanumeric: false,\n\t\tignoreCase: false,\n\t\treferenceType: prefix === dashedPrefix ? undefined : 'branch',\n\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, resource.name),\n\t\ttype: 'issue',\n\t\tdescription: l10n.t('{0} Issue {1}', resource.name, `${dashedPrefix}<num>`),\n\t\tdescriptor: { ...resource },\n\t}));\n}\n"],"mappings":";;AAIA,SAAgB,8BACf,KACA,KACA,UACsB;CACtB,MAAM,eAAe,GAAG,IAAI;CAC5B,OAAO,CAAC,cAAc,GAAG,IAAI,EAAE,CAAC,CAAC,KAAuB,YAAW;EAC1D;EACH;EACL,cAAc;EACd,YAAY;EACZ,eAAe,WAAW,eAAe,KAAA,IAAY;EACrD,OAAO,KAAK,EAAE,yBAAyB,GAAG,aAAa,QAAQ,SAAS,IAAI;EAC5E,MAAM;EACN,aAAa,KAAK,EAAE,iBAAiB,SAAS,MAAM,GAAG,aAAa,MAAM;EAC1E,YAAY,EAAE,GAAG,SAAS;CAC3B,EAAE;AACH"}
@@ -0,0 +1,7 @@
1
+ import { RefSet } from "../../../git/models/autolink.js";
2
+ import { ProviderReadContext } from "./context.js";
3
+ import { GetAutolinkReferencesOptions } from "../manager.js";
4
+ //#region ../plus/integrations/src/reads/autolinks.d.ts
5
+ export declare function getAutolinkReferences(ctx: ProviderReadContext, options: GetAutolinkReferencesOptions): Promise<RefSet | undefined>;
6
+ //#endregion
7
+ //# sourceMappingURL=autolinks.d.ts.map
@@ -0,0 +1,40 @@
1
+ import "../constants.js";
2
+ import { isIncompleteCollection } from "../collectionMetadata.js";
3
+ import { isIssuesIntegration } from "../models/issuesIntegration.js";
4
+ import { createIssueAutolinkReferences } from "../providers/utils/issueAutolinks.js";
5
+ //#region ../plus/integrations/src/reads/autolinks.ts
6
+ async function getAutolinkReferences(ctx, options) {
7
+ if (options.providerId !== "jira" && options.providerId !== "jira-server") return;
8
+ const integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);
9
+ if (integration == null || !isIssuesIntegration(integration)) return void 0;
10
+ try {
11
+ await ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId, { throwOnFailure: true });
12
+ } catch {
13
+ return;
14
+ } finally {
15
+ if (options.forceSync) integration.invalidateDiscoveryForConnection(options.connectionId);
16
+ }
17
+ const result = await integration.getUncachedDiscoveryResult(() => true, options.connectionId);
18
+ const discovery = result?.value;
19
+ if (result?.error != null || discovery?.resources == null || discovery.projects == null || discovery.generation !== integration.getSessionDiscoveryGeneration(discovery.sessionId, options.connectionId, discovery.sessionCloud) || isIncompleteCollection(discovery.projects.metadata)) return;
20
+ const refs = [];
21
+ for (const project of discovery.projects.values) {
22
+ if (options.providerId === "jira-server" && project.key === project.id) continue;
23
+ const resource = discovery.resources.find((r) => r.id === project.resourceId);
24
+ if (resource == null || typeof resource.name !== "string" || typeof resource.url !== "string") return void 0;
25
+ refs.push(...createIssueAutolinkReferences(project.key, `${resource.url}/browse/${project.key}-<num>`, {
26
+ ...resource,
27
+ name: resource.name
28
+ }));
29
+ }
30
+ return [{
31
+ id: integration.id,
32
+ name: integration.name,
33
+ domain: integration.domain,
34
+ icon: integration.icon
35
+ }, refs];
36
+ }
37
+ //#endregion
38
+ export { getAutolinkReferences };
39
+
40
+ //# sourceMappingURL=autolinks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autolinks.js","names":[],"sources":["../../../../../plus/integrations/src/reads/autolinks.ts"],"sourcesContent":["import type { RefSet } from '@gitlens/git/models/autolink.js';\nimport { isIncompleteCollection } from '../collectionMetadata.js';\nimport { IssuesCloudHostIntegrationId, IssuesSelfManagedHostIntegrationId } from '../constants.js';\nimport type { GetAutolinkReferencesOptions } from '../manager.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { createIssueAutolinkReferences } from '../providers/utils/issueAutolinks.js';\nimport type { ProviderReadContext } from './context.js';\n\nexport async function getAutolinkReferences(\n\tctx: ProviderReadContext,\n\toptions: GetAutolinkReferencesOptions,\n): Promise<RefSet | undefined> {\n\tif (\n\t\toptions.providerId !== IssuesCloudHostIntegrationId.Jira &&\n\t\toptions.providerId !== IssuesSelfManagedHostIntegrationId.JiraServer\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null || !isIssuesIntegration(integration)) return undefined;\n\n\ttry {\n\t\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId, {\n\t\t\tthrowOnFailure: true,\n\t\t});\n\t} catch {\n\t\treturn undefined;\n\t} finally {\n\t\tif (options.forceSync) {\n\t\t\t// Completion retires reads started during the refresh, including when it failed.\n\t\t\tintegration.invalidateDiscoveryForConnection(options.connectionId);\n\t\t}\n\t}\n\n\tconst result = await integration.getUncachedDiscoveryResult(() => true, options.connectionId);\n\tconst discovery = result?.value;\n\tif (\n\t\tresult?.error != null ||\n\t\tdiscovery?.resources == null ||\n\t\tdiscovery.projects == null ||\n\t\tdiscovery.generation !==\n\t\t\tintegration.getSessionDiscoveryGeneration(\n\t\t\t\tdiscovery.sessionId,\n\t\t\t\toptions.connectionId,\n\t\t\t\tdiscovery.sessionCloud,\n\t\t\t) ||\n\t\tisIncompleteCollection(discovery.projects.metadata)\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst refs: RefSet[1] = [];\n\tfor (const project of discovery.projects.values) {\n\t\tif (options.providerId === IssuesSelfManagedHostIntegrationId.JiraServer && project.key === project.id) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst resource = discovery.resources.find(r => r.id === project.resourceId);\n\t\tif (resource == null || typeof resource.name !== 'string' || typeof resource.url !== 'string') return undefined;\n\n\t\trefs.push(\n\t\t\t...createIssueAutolinkReferences(project.key, `${resource.url}/browse/${project.key}-<num>`, {\n\t\t\t\t...resource,\n\t\t\t\tname: resource.name,\n\t\t\t}),\n\t\t);\n\t}\n\n\treturn [{ id: integration.id, name: integration.name, domain: integration.domain, icon: integration.icon }, refs];\n}\n"],"mappings":";;;;;AAQA,eAAsB,sBACrB,KACA,SAC8B;CAC9B,IACC,QAAQ,eAAA,UACR,QAAQ,eAAA,eAER;CAGD,MAAM,cAAc,MAAM,IAAI,sBAAsB,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CAC5G,IAAI,eAAe,QAAQ,CAAC,oBAAoB,WAAW,GAAG,OAAO,KAAA;CAErE,IAAI;EACH,MAAM,IAAI,wBAAwB,aAAa,QAAQ,WAAW,QAAQ,cAAc,EACvF,gBAAgB,KACjB,CAAC;CACF,QAAQ;EACP;CACD,UAAU;EACT,IAAI,QAAQ,WAEX,YAAY,iCAAiC,QAAQ,YAAY;CAEnE;CAEA,MAAM,SAAS,MAAM,YAAY,iCAAiC,MAAM,QAAQ,YAAY;CAC5F,MAAM,YAAY,QAAQ;CAC1B,IACC,QAAQ,SAAS,QACjB,WAAW,aAAa,QACxB,UAAU,YAAY,QACtB,UAAU,eACT,YAAY,8BACX,UAAU,WACV,QAAQ,cACR,UAAU,YACX,KACD,uBAAuB,UAAU,SAAS,QAAQ,GAElD;CAGD,MAAM,OAAkB,CAAC;CACzB,KAAK,MAAM,WAAW,UAAU,SAAS,QAAQ;EAChD,IAAI,QAAQ,eAAA,iBAAgE,QAAQ,QAAQ,QAAQ,IACnG;EAGD,MAAM,WAAW,UAAU,UAAU,MAAK,MAAK,EAAE,OAAO,QAAQ,UAAU;EAC1E,IAAI,YAAY,QAAQ,OAAO,SAAS,SAAS,YAAY,OAAO,SAAS,QAAQ,UAAU,OAAO,KAAA;EAEtG,KAAK,KACJ,GAAG,8BAA8B,QAAQ,KAAK,GAAG,SAAS,IAAI,UAAU,QAAQ,IAAI,SAAS;GAC5F,GAAG;GACH,MAAM,SAAS;EAChB,CAAC,CACF;CACD;CAEA,OAAO,CAAC;EAAE,IAAI,YAAY;EAAI,MAAM,YAAY;EAAM,QAAQ,YAAY;EAAQ,MAAM,YAAY;CAAK,GAAG,IAAI;AACjH"}
@@ -43,8 +43,10 @@ export interface ProviderReadContext {
43
43
  warnings: ProviderWarning[];
44
44
  fetchFailed: boolean;
45
45
  };
46
- /** Forces a session refresh before a read when the caller asked for one. Best-effort; never throws. */
47
- forceRefreshIfRequested(integration: Integration, forceSync: boolean | undefined, connectionId: string | undefined): Promise<void>;
46
+ /** Forces a session refresh before a read when the caller asked for one. Best-effort unless strict failure propagation is requested. */
47
+ forceRefreshIfRequested(integration: Integration, forceSync: boolean | undefined, connectionId: string | undefined, options?: {
48
+ throwOnFailure?: boolean;
49
+ }): Promise<void>;
48
50
  /** The configured connection descriptors, as `getConfigured` exposes them. */
49
51
  getConfigured(id?: IntegrationIds, options?: {
50
52
  cloud?: boolean;
@@ -1,7 +1,7 @@
1
1
  import "../constants.js";
2
2
  import { getIssueFieldPresence } from "../fieldPresence.js";
3
3
  import { issueNamesPeople } from "../providers/models.js";
4
- import { issueEtagIncludes, pullRequestEtagIncludes, pullRequestRevision } from "../models/integration.js";
4
+ import { issueEtagIncludes, linearSyncedWithEtagKeys, pullRequestEtagIncludes, pullRequestRevision } from "../models/integration.js";
5
5
  //#region ../plus/integrations/src/reads/etag.ts
6
6
  /**
7
7
  * The etags the batch reads hand back (`getPullRequestsBatch`, `getIssuesBatch`): an opaque stamp of an item's
@@ -46,6 +46,7 @@ function issueEtagFieldsFromShape(issue) {
46
46
  fields.descriptionRule = 1;
47
47
  if (!issue.viewerUnresolved && (issue.viewer != null || !issueNamesPeople(issue))) fields.viewerRule = 1;
48
48
  }
49
+ if (issue.provider?.id === "linear") fields.syncedWithKeys = issue.syncedWith != null ? linearSyncedWithEtagKeys(issue.syncedWith) : null;
49
50
  return fields;
50
51
  }
51
52
  /**
@@ -99,6 +100,7 @@ function issueEtag(fields, includes) {
99
100
  const state = [fields.state, timeOf(fields.updatedDate)];
100
101
  if (fields.descriptionRule != null) state.push(fields.descriptionRule);
101
102
  if (fields.viewerRule != null) state.push(fields.viewerRule);
103
+ if (fields.syncedWithKeys !== void 0) state.push(fields.syncedWithKeys === null ? "synced-with:unknown" : ["synced-with", ...fields.syncedWithKeys]);
102
104
  if (covered.length === 0) return `is1:${JSON.stringify(state)}`;
103
105
  for (const include of covered) switch (include) {
104
106
  case "reactions": state.push(fields.thumbsUpCount ?? null);
@@ -1 +1 @@
1
- {"version":3,"file":"etag.js","names":[],"sources":["../../../../../plus/integrations/src/reads/etag.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { PullRequest, PullRequestShape } from '@gitlens/git/models/pullRequest.js';\nimport { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';\nimport { getIssueFieldPresence } from '../fieldPresence.js';\nimport type {\n\tIssueEtagFields,\n\tIssueEtagInclude,\n\tPullRequestEtagFields,\n\tPullRequestEtagInclude,\n} from '../models/integration.js';\nimport {\n\tazureWorkItemDescriptionRule,\n\tazureWorkItemViewerRule,\n\tissueEtagIncludes,\n\tpullRequestEtagIncludes,\n\tpullRequestRevision,\n} from '../models/integration.js';\nimport { issueNamesPeople } from '../providers/models.js';\n\n/**\n * The etags the batch reads hand back (`getPullRequestsBatch`, `getIssuesBatch`): an opaque stamp of an item's\n * change state, which a caller sends back so a cheap check can answer \"unchanged\" without a full read.\n *\n * Computed by core, never the provider's HTTP ETag, and from the fields alone — not the item's id, since the\n * caller's key already identifies the target and ids cross vocabularies between reads. Not hashed: the stamp IS\n * the serialized fields, so two different change states can never collide into a false `unchanged` — except for an\n * Azure DevOps pull request's `revision`, a 64-bit hash of the fields Azure changes without a timestamp, where a\n * collision (about 2^-64) is the only way a change can hide. The prefix names the scheme (`pr1`, `is1`) and every\n * include it covers ({@link PullRequestEtagInclude}, {@link IssueEtagInclude}), in canonical order\n * (`pr1+mergeable+checks:`, `is1+reactions:`), so two different sets can never collide even when their values do (a\n * `null` mergeability and a `null` rollup). A stamp from another scheme or another set just compares unequal and\n * costs a full read.\n */\n\n/**\n * Azure DevOps: a pull request there keeps no update time, so its etag adds a {@link pullRequestRevision}, and a work\n * item's etag adds the rule its body is read by (see `azureWorkItemDescriptionRule`).\n */\nconst azureProviderIds: ReadonlySet<string> = new Set([\n\tGitCloudHostIntegrationId.AzureDevOps,\n\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n]);\n\nexport function pullRequestEtagFieldsFromShape(pr: PullRequestShape): PullRequestEtagFields {\n\tconst fields: PullRequestEtagFields = {\n\t\tstate: pr.state,\n\t\tisDraft: pr.isDraft,\n\t\tupdatedDate: pr.updatedDate,\n\t\theadSha: pr.refs?.head.sha,\n\t\tmergeableState: pr.mergeableState,\n\t\treviewDecision: pr.reviewDecision,\n\t\t// On the class, not the shape: every provider's mapper builds a `PullRequest`. Read by name rather than by\n\t\t// `instanceof`, which a second copy of the model module fails, silently dropping the rollup from every full\n\t\t// row's etag so that no cheap check ever matches it again.\n\t\tstatusCheckRollupState: 'statusCheckRollupState' in pr ? (pr as PullRequest).statusCheckRollupState : undefined,\n\t};\n\t// Read defensively, as `timeOf` reads the date: a row without a provider must not fail the read.\n\tif (pr.provider != null && azureProviderIds.has(pr.provider.id)) {\n\t\tfields.revision = pullRequestRevision(pr);\n\t}\n\tif (pr.viewerUnresolved) {\n\t\tfields.viewerUnresolved = true;\n\t}\n\treturn fields;\n}\n\nexport function issueEtagFieldsFromShape(issue: IssueShape): IssueEtagFields {\n\tconst fields: IssueEtagFields = { state: issue.state, updatedDate: issue.updatedDate };\n\t// Only where the row's read really fetched reactions: elsewhere a count is a placeholder or a tracker's votes,\n\t// which no cheap check reads, so taking it would fail every check of that row under `'reactions'`.\n\tif (\n\t\tissue.thumbsUpCount != null &&\n\t\tissue.provider != null &&\n\t\tgetIssueFieldPresence(issue)?.reactions === 'fetched'\n\t) {\n\t\tfields.thumbsUpCount = issue.thumbsUpCount;\n\t}\n\tif (issue.provider != null && azureProviderIds.has(issue.provider.id)) {\n\t\tfields.descriptionRule = azureWorkItemDescriptionRule;\n\t\t// A row read while its organization's identity couldn't be must not compare unchanged, or it would be kept\n\t\t// without a viewer until the work item changes. Also a row that names someone and has neither field: one held\n\t\t// from before `viewerUnresolved` existed.\n\t\tif (!issue.viewerUnresolved && (issue.viewer != null || !issueNamesPeople(issue))) {\n\t\t\tfields.viewerRule = azureWorkItemViewerRule;\n\t\t}\n\t}\n\treturn fields;\n}\n\n/**\n * The distinct `includes` in canonical order, whatever order and repetition the caller gave. Values it doesn't know\n * are dropped, so a caller validates them first (see {@link findInvalidPullRequestEtagInclude}).\n */\nexport function normalizePullRequestEtagIncludes(\n\tincludes: readonly PullRequestEtagInclude[],\n): readonly PullRequestEtagInclude[] {\n\treturn pullRequestEtagIncludes.filter(include => includes.includes(include));\n}\n\n/** The first of `includes` that isn't a {@link PullRequestEtagInclude}, if any. */\nexport function findInvalidPullRequestEtagInclude(includes: readonly string[]): string | undefined {\n\treturn includes.find(include => !(pullRequestEtagIncludes as readonly string[]).includes(include));\n}\n\n/**\n * Each of `includes` adds one input — mergeability, review decision or check rollup — which a host may change\n * without moving `updatedDate`. An input that isn't listed is left out, so its churn never forces a full read.\n */\nexport function pullRequestEtag(fields: PullRequestEtagFields, includes: readonly PullRequestEtagInclude[]): string {\n\tconst covered = normalizePullRequestEtagIncludes(includes);\n\tconst state: unknown[] = [fields.state, fields.isDraft ?? null, timeOf(fields.updatedDate), fields.headSha ?? null];\n\t// Only where it is set, so every other host's etag stays exactly what it was.\n\tif (fields.revision != null) {\n\t\tstate.push(fields.revision);\n\t}\n\tif (fields.viewerUnresolved) {\n\t\tstate.push('viewer-unresolved');\n\t}\n\n\tif (covered.length === 0) return `pr1:${JSON.stringify(state)}`;\n\n\tfor (const include of covered) {\n\t\tswitch (include) {\n\t\t\tcase 'mergeable':\n\t\t\t\tstate.push(fields.mergeableState ?? null);\n\t\t\t\tbreak;\n\t\t\tcase 'reviewDecision':\n\t\t\t\tstate.push(fields.reviewDecision ?? null);\n\t\t\t\tbreak;\n\t\t\tcase 'checks':\n\t\t\t\tstate.push(fields.statusCheckRollupState ?? null);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn `pr1+${covered.join('+')}:${JSON.stringify(state)}`;\n}\n\n/** The issue twin of {@link normalizePullRequestEtagIncludes}; validate with {@link findInvalidIssueEtagInclude}. */\nexport function normalizeIssueEtagIncludes(includes: readonly IssueEtagInclude[]): readonly IssueEtagInclude[] {\n\treturn issueEtagIncludes.filter(include => includes.includes(include));\n}\n\n/** The first of `includes` that isn't an {@link IssueEtagInclude}, if any. */\nexport function findInvalidIssueEtagInclude(includes: readonly string[]): string | undefined {\n\treturn includes.find(include => !(issueEtagIncludes as readonly string[]).includes(include));\n}\n\n/** `'reactions'` adds the thumbs-up count, which a host changes without moving `updatedDate`. */\nexport function issueEtag(fields: IssueEtagFields, includes: readonly IssueEtagInclude[]): string {\n\tconst covered = normalizeIssueEtagIncludes(includes);\n\tconst state: unknown[] = [fields.state, timeOf(fields.updatedDate)];\n\t// Only where it is set, so every other provider's etag stays exactly what it was.\n\tif (fields.descriptionRule != null) {\n\t\tstate.push(fields.descriptionRule);\n\t}\n\tif (fields.viewerRule != null) {\n\t\tstate.push(fields.viewerRule);\n\t}\n\tif (covered.length === 0) return `is1:${JSON.stringify(state)}`;\n\n\tfor (const include of covered) {\n\t\tswitch (include) {\n\t\t\tcase 'reactions':\n\t\t\t\tstate.push(fields.thumbsUpCount ?? null);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn `is1+${covered.join('+')}:${JSON.stringify(state)}`;\n}\n\n/**\n * Never throws: an etag is computed for every fully read row, so a mapper that hands over a missing or unparseable\n * date must not fail the whole read. Such a row's etag just won't match a cheap check's, which always has a date,\n * so the cost is a full read, never a false `unchanged`.\n */\nfunction timeOf(date: Date): number | null {\n\tconst time = new Date(date).getTime();\n\treturn Number.isNaN(time) ? null : time;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"etag.js","names":[],"sources":["../../../../../plus/integrations/src/reads/etag.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { PullRequest, PullRequestShape } from '@gitlens/git/models/pullRequest.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tIssuesCloudHostIntegrationId,\n} from '../constants.js';\nimport { getIssueFieldPresence } from '../fieldPresence.js';\nimport type {\n\tIssueEtagFields,\n\tIssueEtagInclude,\n\tPullRequestEtagFields,\n\tPullRequestEtagInclude,\n} from '../models/integration.js';\nimport {\n\tazureWorkItemDescriptionRule,\n\tazureWorkItemViewerRule,\n\tissueEtagIncludes,\n\tlinearSyncedWithEtagKeys,\n\tpullRequestEtagIncludes,\n\tpullRequestRevision,\n} from '../models/integration.js';\nimport { issueNamesPeople } from '../providers/models.js';\n\n/**\n * The etags the batch reads hand back (`getPullRequestsBatch`, `getIssuesBatch`): an opaque stamp of an item's\n * change state, which a caller sends back so a cheap check can answer \"unchanged\" without a full read.\n *\n * Computed by core, never the provider's HTTP ETag, and from the fields alone — not the item's id, since the\n * caller's key already identifies the target and ids cross vocabularies between reads. Not hashed: the stamp IS\n * the serialized fields, so two different change states can never collide into a false `unchanged` — except for an\n * Azure DevOps pull request's `revision`, a 64-bit hash of the fields Azure changes without a timestamp, where a\n * collision (about 2^-64) is the only way a change can hide. The prefix names the scheme (`pr1`, `is1`) and every\n * include it covers ({@link PullRequestEtagInclude}, {@link IssueEtagInclude}), in canonical order\n * (`pr1+mergeable+checks:`, `is1+reactions:`), so two different sets can never collide even when their values do (a\n * `null` mergeability and a `null` rollup). A stamp from another scheme or another set just compares unequal and\n * costs a full read.\n */\n\n/**\n * Azure DevOps: a pull request there keeps no update time, so its etag adds a {@link pullRequestRevision}, and a work\n * item's etag adds the rule its body is read by (see `azureWorkItemDescriptionRule`).\n */\nconst azureProviderIds: ReadonlySet<string> = new Set([\n\tGitCloudHostIntegrationId.AzureDevOps,\n\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n]);\n\nexport function pullRequestEtagFieldsFromShape(pr: PullRequestShape): PullRequestEtagFields {\n\tconst fields: PullRequestEtagFields = {\n\t\tstate: pr.state,\n\t\tisDraft: pr.isDraft,\n\t\tupdatedDate: pr.updatedDate,\n\t\theadSha: pr.refs?.head.sha,\n\t\tmergeableState: pr.mergeableState,\n\t\treviewDecision: pr.reviewDecision,\n\t\t// On the class, not the shape: every provider's mapper builds a `PullRequest`. Read by name rather than by\n\t\t// `instanceof`, which a second copy of the model module fails, silently dropping the rollup from every full\n\t\t// row's etag so that no cheap check ever matches it again.\n\t\tstatusCheckRollupState: 'statusCheckRollupState' in pr ? (pr as PullRequest).statusCheckRollupState : undefined,\n\t};\n\t// Read defensively, as `timeOf` reads the date: a row without a provider must not fail the read.\n\tif (pr.provider != null && azureProviderIds.has(pr.provider.id)) {\n\t\tfields.revision = pullRequestRevision(pr);\n\t}\n\tif (pr.viewerUnresolved) {\n\t\tfields.viewerUnresolved = true;\n\t}\n\treturn fields;\n}\n\nexport function issueEtagFieldsFromShape(issue: IssueShape): IssueEtagFields {\n\tconst fields: IssueEtagFields = { state: issue.state, updatedDate: issue.updatedDate };\n\t// Only where the row's read really fetched reactions: elsewhere a count is a placeholder or a tracker's votes,\n\t// which no cheap check reads, so taking it would fail every check of that row under `'reactions'`.\n\tif (\n\t\tissue.thumbsUpCount != null &&\n\t\tissue.provider != null &&\n\t\tgetIssueFieldPresence(issue)?.reactions === 'fetched'\n\t) {\n\t\tfields.thumbsUpCount = issue.thumbsUpCount;\n\t}\n\tif (issue.provider != null && azureProviderIds.has(issue.provider.id)) {\n\t\tfields.descriptionRule = azureWorkItemDescriptionRule;\n\t\t// A row read while its organization's identity couldn't be must not compare unchanged, or it would be kept\n\t\t// without a viewer until the work item changes. Also a row that names someone and has neither field: one held\n\t\t// from before `viewerUnresolved` existed.\n\t\tif (!issue.viewerUnresolved && (issue.viewer != null || !issueNamesPeople(issue))) {\n\t\t\tfields.viewerRule = azureWorkItemViewerRule;\n\t\t}\n\t}\n\tif (issue.provider?.id === IssuesCloudHostIntegrationId.Linear) {\n\t\tfields.syncedWithKeys = issue.syncedWith != null ? linearSyncedWithEtagKeys(issue.syncedWith) : null;\n\t}\n\treturn fields;\n}\n\n/**\n * The distinct `includes` in canonical order, whatever order and repetition the caller gave. Values it doesn't know\n * are dropped, so a caller validates them first (see {@link findInvalidPullRequestEtagInclude}).\n */\nexport function normalizePullRequestEtagIncludes(\n\tincludes: readonly PullRequestEtagInclude[],\n): readonly PullRequestEtagInclude[] {\n\treturn pullRequestEtagIncludes.filter(include => includes.includes(include));\n}\n\n/** The first of `includes` that isn't a {@link PullRequestEtagInclude}, if any. */\nexport function findInvalidPullRequestEtagInclude(includes: readonly string[]): string | undefined {\n\treturn includes.find(include => !(pullRequestEtagIncludes as readonly string[]).includes(include));\n}\n\n/**\n * Each of `includes` adds one input — mergeability, review decision or check rollup — which a host may change\n * without moving `updatedDate`. An input that isn't listed is left out, so its churn never forces a full read.\n */\nexport function pullRequestEtag(fields: PullRequestEtagFields, includes: readonly PullRequestEtagInclude[]): string {\n\tconst covered = normalizePullRequestEtagIncludes(includes);\n\tconst state: unknown[] = [fields.state, fields.isDraft ?? null, timeOf(fields.updatedDate), fields.headSha ?? null];\n\t// Only where it is set, so every other host's etag stays exactly what it was.\n\tif (fields.revision != null) {\n\t\tstate.push(fields.revision);\n\t}\n\tif (fields.viewerUnresolved) {\n\t\tstate.push('viewer-unresolved');\n\t}\n\n\tif (covered.length === 0) return `pr1:${JSON.stringify(state)}`;\n\n\tfor (const include of covered) {\n\t\tswitch (include) {\n\t\t\tcase 'mergeable':\n\t\t\t\tstate.push(fields.mergeableState ?? null);\n\t\t\t\tbreak;\n\t\t\tcase 'reviewDecision':\n\t\t\t\tstate.push(fields.reviewDecision ?? null);\n\t\t\t\tbreak;\n\t\t\tcase 'checks':\n\t\t\t\tstate.push(fields.statusCheckRollupState ?? null);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn `pr1+${covered.join('+')}:${JSON.stringify(state)}`;\n}\n\n/** The issue twin of {@link normalizePullRequestEtagIncludes}; validate with {@link findInvalidIssueEtagInclude}. */\nexport function normalizeIssueEtagIncludes(includes: readonly IssueEtagInclude[]): readonly IssueEtagInclude[] {\n\treturn issueEtagIncludes.filter(include => includes.includes(include));\n}\n\n/** The first of `includes` that isn't an {@link IssueEtagInclude}, if any. */\nexport function findInvalidIssueEtagInclude(includes: readonly string[]): string | undefined {\n\treturn includes.find(include => !(issueEtagIncludes as readonly string[]).includes(include));\n}\n\n/** `'reactions'` adds the thumbs-up count, which a host changes without moving `updatedDate`. */\nexport function issueEtag(fields: IssueEtagFields, includes: readonly IssueEtagInclude[]): string {\n\tconst covered = normalizeIssueEtagIncludes(includes);\n\tconst state: unknown[] = [fields.state, timeOf(fields.updatedDate)];\n\t// Only where it is set, so every other provider's etag stays exactly what it was.\n\tif (fields.descriptionRule != null) {\n\t\tstate.push(fields.descriptionRule);\n\t}\n\tif (fields.viewerRule != null) {\n\t\tstate.push(fields.viewerRule);\n\t}\n\t// Only where it is set, so every other provider's etag stays exactly what it was. An unknown list is spelled\n\t// apart from every known one, so it never compares unchanged.\n\tif (fields.syncedWithKeys !== undefined) {\n\t\tstate.push(fields.syncedWithKeys === null ? 'synced-with:unknown' : ['synced-with', ...fields.syncedWithKeys]);\n\t}\n\tif (covered.length === 0) return `is1:${JSON.stringify(state)}`;\n\n\tfor (const include of covered) {\n\t\tswitch (include) {\n\t\t\tcase 'reactions':\n\t\t\t\tstate.push(fields.thumbsUpCount ?? null);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn `is1+${covered.join('+')}:${JSON.stringify(state)}`;\n}\n\n/**\n * Never throws: an etag is computed for every fully read row, so a mapper that hands over a missing or unparseable\n * date must not fail the whole read. Such a row's etag just won't match a cheap check's, which always has a date,\n * so the cost is a full read, never a false `unchanged`.\n */\nfunction timeOf(date: Date): number | null {\n\tconst time = new Date(date).getTime();\n\treturn Number.isNaN(time) ? null : time;\n}\n"],"mappings":"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"}
@@ -4,10 +4,10 @@ import { appendDedupedWarning } from "../results.js";
4
4
  import { mergeAssessmentInto } from "../collectionMetadata.js";
5
5
  import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
6
6
  import { toProviderRepositoryShape } from "../providers/models.js";
7
+ import { isIssuesIntegration } from "../models/issuesIntegration.js";
7
8
  import { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from "./paging.js";
8
9
  import { gitHostOnlySurfaceWarning, truncationWarning, unsupportedWarning } from "./warnings.js";
9
10
  import { discoverResources, runCaptured } from "./drains.js";
10
- import { isIssuesIntegration } from "../models/issuesIntegration.js";
11
11
  import { assertHierarchyReadTarget, mergeHierarchyResults, projectToOrg, resourceMatchesOrg, resourceToOrg, withProviderContext } from "./hierarchy.utils.js";
12
12
  //#region ../plus/integrations/src/reads/hierarchy.ts
13
13
  /**
@@ -3,9 +3,9 @@ import { githubGraphQLInt32Max, isAzureProviderId, isGitHubProviderId } from "..
3
3
  import { areDomainsOnSameHost, hostFromDomain } from "../utils/domain.utils.js";
4
4
  import { isGitHostIntegration, isIssuesHostIntegrationId, isIssuesSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
5
5
  import { issueEtagIncludes } from "../models/integration.js";
6
+ import { isIssuesIntegration } from "../models/issuesIntegration.js";
6
7
  import { gitHostOnlySurfaceWarning, issueTrackerOnlySurfaceWarning, issuesUnsupportedWarning, noConnectionWarning, otherWarning, unsupportedWarning } from "./warnings.js";
7
8
  import { runCaptured } from "./drains.js";
8
- import { isIssuesIntegration } from "../models/issuesIntegration.js";
9
9
  import { readEtaggedBatch } from "./batchEtags.js";
10
10
  import { findInvalidIssueEtagInclude, issueEtag, issueEtagFieldsFromShape, normalizeIssueEtagIncludes } from "./etag.js";
11
11
  //#region ../plus/integrations/src/reads/issueBatch.ts
@@ -7,10 +7,10 @@ import "../providerFilters.js";
7
7
  import { isIssuesHostIntegrationId } from "../utils/integration.utils.js";
8
8
  import { mergeCollectionMetadata, parsePageCursor } from "../providers/utils/providerPaging.js";
9
9
  import { providersMetadata } from "../providers/models.js";
10
+ import { isIssuesIntegration } from "../models/issuesIntegration.js";
10
11
  import { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from "./cursors.js";
11
12
  import { incompleteReadWarning, issueTrackerOnlySurfaceWarning, otherWarning, projectCutWarning, unmatchedSelectorWarning, unmergeableIssueSortWarning, unsupportedIssueSortWarning, unsupportedWarning } from "./warnings.js";
12
13
  import { discoverResources, runCaptured } from "./drains.js";
13
- import { isIssuesIntegration } from "../models/issuesIntegration.js";
14
14
  import { projectKey, resourceIdForProject, resourceLabel, selectorMatches } from "./hierarchy.utils.js";
15
15
  import { resolveIssueSort, toIssueOrdering } from "./ordering.js";
16
16
  import { confirmCredential, confirmSelection, toSelectionAction } from "./selectionConfirmation.js";
@@ -7,8 +7,8 @@ import { mergeAssessmentInto } from "../collectionMetadata.js";
7
7
  import { decodePathSegment, getRemoteHostMatcher, hostFromDomain, isWebRemoteScheme } from "../utils/domain.utils.js";
8
8
  import { getIntegrationIdForRemote, getSelfManagedBaseUrl, isGitCloudHostIntegrationId, isGitSelfManagedHostIntegrationId, remoteProviderTypeForConfig, remoteProviderTypeForIntegration } from "../utils/integration.utils.js";
9
9
  import { isAzureCloudDomain, isBitbucketCloudDomain, isGitHubDotCom, isGitLabDotCom } from "../providers/models.js";
10
- import { noConnectionWarning } from "./warnings.js";
11
10
  import { isIssuesIntegration } from "../models/issuesIntegration.js";
11
+ import { noConnectionWarning } from "./warnings.js";
12
12
  import { getAzureRepositoryIdentity } from "../providers/azure/azure.utils.js";
13
13
  //#region ../plus/integrations/src/reads/resolveRepository.ts
14
14
  /**
@@ -228,7 +228,10 @@ The hosts with a cheap check select only the change state:
228
228
  the full read sends one request per issue. The query matches the team's current key, while the full read may also
229
229
  resolve an identifier the issue no longer carries (its team was renamed, or it moved), so an issue the query leaves
230
230
  out, or an identifier that isn't a team key and a number, falls through to its own full read, which decides whether
231
- it is absent.
231
+ it is absent. The check also reads each issue's GitHub sync links (`syncedWith`) with their coordinates, so a link
232
+ added, removed or moved changes the etag even when Linear leaves the update time alone. A row without `syncedWith`
233
+ (held from before the field, or read while Linear failed to resolve its link list) never compares unchanged, so it is
234
+ read in full again.
232
235
 
233
236
  Every other host and tracker accepts the `etag`, reads the target in full, and etags the row. On a host with a cheap check, a call is up to three integration calls: the cheap check
234
237
  of the targets that sent an `etag`, a full read of the rest started alongside it, and a full read of the targets
@@ -1217,7 +1220,28 @@ every read below is scoped to that one connection; a paged read that omits it ge
1217
1220
  while `getIssuesBatch` refuses instead (see §4). `listOrgs` returns exactly one resource — the
1218
1221
  instance itself, synthesized from the configured host rather than fetched — and its projects carry the project key
1219
1222
  as `key` (the numeric id when the server reports none), which is what `listProjects` publishes as `name` and what
1220
- `project` selects; reads address the project by id regardless. No autolinks are registered yet.
1223
+ `project` selects; reads address the project by id regardless. Project autolinks use the real key, skipping
1224
+ projects whose key fell back to the numeric id. Dashed commit references (`PROJ-12`) and underscored branch
1225
+ references (`PROJ_12`) both point to `<configured instance URL>/browse/PROJ-12`, retaining any context path.
1226
+
1227
+ `getAutolinkReferences({ providerId, connectionId?, domain?, forceSync? })` supports Jira Cloud and Data Center.
1228
+ It returns a `RefSet` tuple containing provider metadata and references for that one connection, or `undefined`
1229
+ when the target cannot be resolved or discovery failed or was incomplete. Each call awaits uncached discovery
1230
+ with one resolved session; callers own coalescing and caching. `forceSync: true` refreshes the session and
1231
+ retires discovery started before or during that refresh. Refreshing a secondary connection does not change the
1232
+ primary session. Empty references mean complete discovery found no projects with links. Internal decoration
1233
+ readers retain their separate tolerant cache. Match references in the host with `getAutolinks` or `getBranchAutolinks` from
1234
+ `git/utils/autolink.utils.js`; references may hold compiled regexes and are not a wire format. Match separate
1235
+ references independently if distinct projects sharing an issue number must both be retained, since
1236
+ `getAutolinks` keys its result by the numeric suffix.
1237
+
1238
+ `getAutolinks(message, refsets, { maxResults })` stops static-reference materialization when the result map
1239
+ reaches the requested number of distinct entries, including entries inserted by dynamic callbacks. Omit the
1240
+ option for existing unlimited behavior; zero returns no results. A limited call resets its cached regex so
1241
+ subsequent calls still start at the beginning. Dynamic parse callbacks run to completion and can exceed the
1242
+ requested count; this option cannot limit work inside them. Jira references from this facade are static. Consumers can request
1243
+ one result beyond their display budget to detect overflow, then yield between reference batches to bound
1244
+ work across large catalogs.
1221
1245
 
1222
1246
  ¹ Branches in the base repository only: a target whose `headOwner` names another owner is refused, since Bitbucket DC
1223
1247
  finds a branch's pull requests only through the repository the branch lives in, and an Azure DevOps fork shares its
@@ -1545,6 +1569,14 @@ provider-specific (Azure work-item ids are organization-scoped). For cross-scope
1545
1569
  provider/domain plus repository or project identity and `nodeId`; `url` is also unique for the provider reads
1546
1570
  that require one.
1547
1571
 
1572
+ A Linear row also carries `syncedWith`: the GitHub issues Linear's two-way GitHub Issues sync keeps it in sync
1573
+ with, so a host reading both providers can show the pair once. Each link's `id` is the GitHub issue's database id
1574
+ (its REST `id`), which a GitHub row doesn't carry (its `nodeId` is the GraphQL node ID), so match a GitHub row on
1575
+ `owner`/`repo`/`number`, comparing `owner` and `repo` case-insensitively as GitHub does. A link carries no
1576
+ coordinates when Linear reports it without them or failed to resolve that link's details. Linear's link names no host
1577
+ and Linear also syncs with GitHub Enterprise Cloud, so the same coordinates on two hosts can't be told apart from the
1578
+ row alone. `[]` means synced with nothing; an absent field means unknown, not unsynced.
1579
+
1548
1580
  ## 10. Token-scoped reads (`lite.js`)
1549
1581
 
1550
1582
  For a consumer that already holds a provider token and wants one stateless read — no storage, no session
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gitkraken/core-gitlens",
3
3
  "description": "GitLens core — shared Git / AI / GitHub primitives for internal GitKraken consumption",
4
- "version": "0.20.2",
4
+ "version": "0.22.0",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": {
7
7
  "name": "GitKraken",
@@ -239,7 +239,7 @@
239
239
  }
240
240
  },
241
241
  "dependencies": {
242
- "@gitkraken/provider-apis": "0.65.5",
242
+ "@gitkraken/provider-apis": "0.65.6",
243
243
  "@octokit/graphql": "9.0.4",
244
244
  "@octokit/request": "10.0.14",
245
245
  "@octokit/request-error": "7.1.1",