@gitkraken/core-gitlens 0.4.100 → 0.5.100

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 (122) hide show
  1. package/CHANGELOG.md +12 -4
  2. package/dist/git/models/graph.d.ts +63 -33
  3. package/dist/git/models/graph.d.ts.map +1 -1
  4. package/dist/git/models/graph.js.map +1 -1
  5. package/dist/git/models/graphSession.d.ts +3 -84
  6. package/dist/git/models/graphSession.d.ts.map +1 -1
  7. package/dist/git/models/graphSession.js +1 -6
  8. package/dist/git/models/graphSession.js.map +1 -1
  9. package/dist/git/providers/graph.d.ts +1 -15
  10. package/dist/git/providers/graph.d.ts.map +1 -1
  11. package/dist/git/providers/pausedOperations.d.ts +2 -0
  12. package/dist/git/providers/pausedOperations.d.ts.map +1 -1
  13. package/dist/git/utils/reachability.utils.d.ts +1 -1
  14. package/dist/git/utils/reachability.utils.js +1 -1
  15. package/dist/git-cli/providers/graph.d.ts +1 -3
  16. package/dist/git-cli/providers/graph.d.ts.map +1 -1
  17. package/dist/git-cli/providers/graph.js +37 -214
  18. package/dist/git-cli/providers/graph.js.map +1 -1
  19. package/dist/git-cli/providers/pausedOperations.d.ts +1 -0
  20. package/dist/git-cli/providers/pausedOperations.d.ts.map +1 -1
  21. package/dist/git-cli/providers/pausedOperations.js +21 -1
  22. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  23. package/dist/git-cli/providers/staging.d.ts.map +1 -1
  24. package/dist/git-cli/providers/staging.js +10 -1
  25. package/dist/git-cli/providers/staging.js.map +1 -1
  26. package/dist/plus/git-github/providers/github/graph.d.ts +1 -3
  27. package/dist/plus/git-github/providers/github/graph.d.ts.map +1 -1
  28. package/dist/plus/git-github/providers/github/graph.js +8 -10
  29. package/dist/plus/git-github/providers/github/graph.js.map +1 -1
  30. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts +38 -48
  31. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts.map +1 -1
  32. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +22 -5
  33. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
  34. package/dist/plus/integrations/authentication/rejectedTokenTracker.d.ts +39 -0
  35. package/dist/plus/integrations/authentication/rejectedTokenTracker.d.ts.map +1 -0
  36. package/dist/plus/integrations/authentication/rejectedTokenTracker.js +53 -0
  37. package/dist/plus/integrations/authentication/rejectedTokenTracker.js.map +1 -0
  38. package/dist/plus/integrations/errors.d.ts.map +1 -1
  39. package/dist/plus/integrations/errors.js +6 -2
  40. package/dist/plus/integrations/errors.js.map +1 -1
  41. package/dist/plus/integrations/integrationService.d.ts +8 -5
  42. package/dist/plus/integrations/integrationService.d.ts.map +1 -1
  43. package/dist/plus/integrations/integrationService.js +20 -652
  44. package/dist/plus/integrations/integrationService.js.map +1 -1
  45. package/dist/plus/integrations/models/gitHostIntegration.d.ts +10 -17
  46. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  47. package/dist/plus/integrations/models/gitHostIntegration.js +57 -94
  48. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  49. package/dist/plus/integrations/models/integration.d.ts +20 -0
  50. package/dist/plus/integrations/models/integration.d.ts.map +1 -1
  51. package/dist/plus/integrations/models/integration.js +61 -4
  52. package/dist/plus/integrations/models/integration.js.map +1 -1
  53. package/dist/plus/integrations/models/issuesIntegration.d.ts +4 -21
  54. package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
  55. package/dist/plus/integrations/models/issuesIntegration.js +10 -52
  56. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  57. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  58. package/dist/plus/integrations/reads/broaden.d.ts +20 -0
  59. package/dist/plus/integrations/reads/broaden.d.ts.map +1 -0
  60. package/dist/plus/integrations/reads/broaden.js +326 -0
  61. package/dist/plus/integrations/reads/broaden.js.map +1 -0
  62. package/dist/plus/integrations/reads/context.d.ts +22 -0
  63. package/dist/plus/integrations/reads/context.d.ts.map +1 -1
  64. package/dist/plus/integrations/reads/hierarchy.d.ts +1 -1
  65. package/dist/plus/integrations/reads/hierarchy.d.ts.map +1 -1
  66. package/dist/plus/integrations/reads/hierarchy.js +109 -137
  67. package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
  68. package/dist/plus/integrations/reads/hierarchy.utils.d.ts +25 -1
  69. package/dist/plus/integrations/reads/hierarchy.utils.d.ts.map +1 -1
  70. package/dist/plus/integrations/reads/hierarchy.utils.js +43 -0
  71. package/dist/plus/integrations/reads/hierarchy.utils.js.map +1 -1
  72. package/dist/plus/integrations/reads/issueTracker.d.ts +1 -1
  73. package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
  74. package/dist/plus/integrations/reads/issueTracker.js +13 -25
  75. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  76. package/dist/plus/integrations/reads/issues.d.ts +2 -2
  77. package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
  78. package/dist/plus/integrations/reads/issues.js.map +1 -1
  79. package/dist/plus/integrations/reads/pullRequests.d.ts +1 -1
  80. package/dist/plus/integrations/reads/pullRequests.d.ts.map +1 -1
  81. package/dist/plus/integrations/reads/pullRequests.js +1 -1
  82. package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
  83. package/dist/plus/integrations/reads/resolveRepository.d.ts +25 -0
  84. package/dist/plus/integrations/reads/resolveRepository.d.ts.map +1 -0
  85. package/dist/plus/integrations/reads/resolveRepository.js +235 -0
  86. package/dist/plus/integrations/reads/resolveRepository.js.map +1 -0
  87. package/dist/plus/integrations/reads/sweeps.d.ts +11 -0
  88. package/dist/plus/integrations/reads/sweeps.d.ts.map +1 -0
  89. package/dist/plus/integrations/reads/sweeps.js +125 -0
  90. package/dist/plus/integrations/reads/sweeps.js.map +1 -0
  91. package/dist/utils/date.d.ts +1 -1
  92. package/dist/utils/date.d.ts.map +1 -1
  93. package/dist/utils/date.js +5 -3
  94. package/dist/utils/date.js.map +1 -1
  95. package/package.json +1 -1
  96. package/src/git/models/graph.ts +59 -41
  97. package/src/git/models/graphSession.ts +3 -106
  98. package/src/git/providers/graph.ts +1 -15
  99. package/src/git/providers/pausedOperations.ts +8 -1
  100. package/src/git/utils/reachability.utils.ts +1 -1
  101. package/src/git-cli/providers/graph.ts +37 -250
  102. package/src/git-cli/providers/pausedOperations.ts +29 -2
  103. package/src/git-cli/providers/staging.ts +8 -1
  104. package/src/plus/git-github/providers/github/graph.ts +8 -15
  105. package/src/plus/integrations/authentication/integrationAuthenticationProvider.ts +62 -40
  106. package/src/plus/integrations/authentication/rejectedTokenTracker.ts +56 -0
  107. package/src/plus/integrations/errors.ts +6 -2
  108. package/src/plus/integrations/integrationService.ts +27 -801
  109. package/src/plus/integrations/models/gitHostIntegration.ts +90 -99
  110. package/src/plus/integrations/models/integration.ts +68 -5
  111. package/src/plus/integrations/models/issuesIntegration.ts +10 -57
  112. package/src/plus/integrations/providers/azureDevOps.ts +1 -1
  113. package/src/plus/integrations/reads/broaden.ts +452 -0
  114. package/src/plus/integrations/reads/context.ts +24 -0
  115. package/src/plus/integrations/reads/hierarchy.ts +133 -153
  116. package/src/plus/integrations/reads/hierarchy.utils.ts +50 -1
  117. package/src/plus/integrations/reads/issueTracker.ts +15 -27
  118. package/src/plus/integrations/reads/issues.ts +4 -2
  119. package/src/plus/integrations/reads/pullRequests.ts +2 -9
  120. package/src/plus/integrations/reads/resolveRepository.ts +272 -0
  121. package/src/plus/integrations/reads/sweeps.ts +185 -0
  122. package/src/utils/date.ts +6 -3
@@ -32,6 +32,38 @@ export interface IntegrationAuthenticationSessionDescriptor {
32
32
  [key: string]: unknown;
33
33
  }
34
34
 
35
+ /**
36
+ * Options for {@link IntegrationAuthenticationProvider.getSession}. The two variants keep `sync` exclusive
37
+ * with the interactive flags: a sync resolves whatever the cloud already has, so it must never open a connect
38
+ * window.
39
+ *
40
+ * `refreshRejectedToken` marks the stored token as known-refused by the provider (an `AuthenticationError` on
41
+ * a previous read), which forces a cloud `/refresh` even when the token's expiry still claims it is valid —
42
+ * the case a purely time-based refresh cannot see. The refresh token itself never reaches the client: the GK
43
+ * cloud performs the exchange server-side.
44
+ */
45
+ export type GetSessionOptions =
46
+ | {
47
+ createIfNeeded?: boolean;
48
+ forceNewSession?: boolean;
49
+ sync?: never;
50
+ refreshRejectedToken?: boolean;
51
+ source?: Sources;
52
+ }
53
+ | {
54
+ createIfNeeded?: never;
55
+ forceNewSession?: never;
56
+ sync: boolean;
57
+ refreshRejectedToken?: boolean;
58
+ source?: Sources;
59
+ };
60
+
61
+ /**
62
+ * What {@link IntegrationAuthenticationProviderBase.getSession} passes down once it has decided the stored
63
+ * session can't be served as-is. `refreshIfExpired` is its own conclusion, not a caller's option.
64
+ */
65
+ type GetNewSessionOptions = GetSessionOptions & { refreshIfExpired?: boolean };
66
+
35
67
  export interface IntegrationAuthenticationProvider extends Disposable {
36
68
  /**
37
69
  * Clears the stored secret for one connection only (never the whole provider). Unlike
@@ -46,11 +78,13 @@ export interface IntegrationAuthenticationProvider extends Disposable {
46
78
  options?: { preserveConfigured?: boolean },
47
79
  ): Promise<void>;
48
80
  deleteAllSessions(descriptor?: IntegrationAuthenticationSessionDescriptor): Promise<void>;
81
+ /**
82
+ * Resolves the session for `descriptor`, from storage when it is still usable and from the cloud
83
+ * otherwise. See {@link GetSessionOptions}.
84
+ */
49
85
  getSession(
50
86
  descriptor: IntegrationAuthenticationSessionDescriptor,
51
- options?:
52
- | { createIfNeeded?: boolean; forceNewSession?: boolean; sync?: never; source?: Sources }
53
- | { createIfNeeded?: never; forceNewSession?: never; sync: boolean; source?: Sources },
87
+ options?: GetSessionOptions,
54
88
  ): Promise<ProviderAuthenticationSession | undefined>;
55
89
  get onDidChange(): Event<void>;
56
90
  }
@@ -120,9 +154,7 @@ abstract class IntegrationAuthenticationProviderBase<
120
154
  @trace()
121
155
  async getSession(
122
156
  descriptor: IntegrationAuthenticationSessionDescriptor,
123
- options?:
124
- | { createIfNeeded?: boolean; forceNewSession?: boolean; sync?: never; source?: Sources }
125
- | { createIfNeeded?: never; forceNewSession?: never; sync: boolean; source?: Sources },
157
+ options?: GetSessionOptions,
126
158
  ): Promise<ProviderAuthenticationSession | undefined> {
127
159
  let session;
128
160
  let previousToken;
@@ -143,10 +175,15 @@ abstract class IntegrationAuthenticationProviderBase<
143
175
  }
144
176
 
145
177
  const isExpiredSession = session?.expiresAt != null && new Date(session.expiresAt).getTime() < Date.now();
146
- if (session == null || isExpiredSession) {
178
+ // A rejected token has to re-resolve even when the stored session looks perfectly healthy: that is
179
+ // the whole point — the provider refused it while its expiry still says valid. Without this the
180
+ // stored session short-circuits here and the refresh below is never reached.
181
+ const refreshRejectedToken = options?.refreshRejectedToken === true && session != null;
182
+ if (session == null || isExpiredSession || refreshRejectedToken) {
147
183
  session = await this.getNewSession(descriptor, {
148
184
  ...options,
149
185
  refreshIfExpired: isExpiredSession,
186
+ refreshRejectedToken: refreshRejectedToken,
150
187
  });
151
188
 
152
189
  if (session != null) {
@@ -163,21 +200,7 @@ abstract class IntegrationAuthenticationProviderBase<
163
200
 
164
201
  protected abstract getNewSession(
165
202
  descriptor: IntegrationAuthenticationSessionDescriptor,
166
- options?:
167
- | {
168
- createIfNeeded?: boolean;
169
- forceNewSession?: boolean;
170
- sync?: never;
171
- refreshIfExpired?: boolean;
172
- source?: Sources;
173
- }
174
- | {
175
- createIfNeeded?: never;
176
- forceNewSession?: never;
177
- sync: boolean;
178
- refreshIfExpired?: boolean;
179
- source?: Sources;
180
- },
203
+ options?: GetNewSessionOptions,
181
204
  ): Promise<ProviderAuthenticationSession | undefined>;
182
205
 
183
206
  protected fireChange(): void {
@@ -202,21 +225,7 @@ export class CloudIntegrationAuthenticationProvider<
202
225
 
203
226
  protected override async getNewSession(
204
227
  descriptor: IntegrationAuthenticationSessionDescriptor,
205
- options?:
206
- | {
207
- createIfNeeded?: boolean;
208
- forceNewSession?: boolean;
209
- sync?: never;
210
- refreshIfExpired?: boolean;
211
- source?: Sources;
212
- }
213
- | {
214
- createIfNeeded?: never;
215
- forceNewSession?: never;
216
- sync: boolean;
217
- refreshIfExpired?: boolean;
218
- source?: Sources;
219
- },
228
+ options?: GetNewSessionOptions,
220
229
  ): Promise<ProviderAuthenticationSession | undefined> {
221
230
  if (options?.forceNewSession) {
222
231
  if ((await this.disconnectCloudSession()) === 'failure') {
@@ -230,9 +239,15 @@ export class CloudIntegrationAuthenticationProvider<
230
239
  // TODO: This is a stopgap to make sure we're not hammering the api on automatic calls to get the session.
231
240
  // Ultimately we want to timestamp calls to syncCloudIntegrations and use that to determine whether we should
232
241
  // make the call or not.
242
+ // `refreshRejectedToken` joins the list for the same reason `refreshIfExpired` is on it: both mean the
243
+ // session in hand is known-unusable, so skipping the cloud round trip would just re-serve it.
233
244
  let session =
234
- options?.refreshIfExpired || options?.createIfNeeded || options?.forceNewSession || options?.sync
235
- ? await this.getCloudSession(descriptor)
245
+ options?.refreshIfExpired ||
246
+ options?.refreshRejectedToken ||
247
+ options?.createIfNeeded ||
248
+ options?.forceNewSession ||
249
+ options?.sync
250
+ ? await this.getCloudSession(descriptor, { refreshRejectedToken: options?.refreshRejectedToken })
236
251
  : undefined;
237
252
 
238
253
  const shouldCreateSession = options?.createIfNeeded && session == null;
@@ -269,6 +284,7 @@ export class CloudIntegrationAuthenticationProvider<
269
284
 
270
285
  private async getCloudSession(
271
286
  descriptor: IntegrationAuthenticationSessionDescriptor,
287
+ options?: { refreshRejectedToken?: boolean },
272
288
  ): Promise<ProviderAuthenticationSession | undefined> {
273
289
  const loggedIn = (await this.authenticationService.ctx.account.getAccount()) != null;
274
290
  if (!loggedIn) return undefined;
@@ -304,7 +320,13 @@ export class CloudIntegrationAuthenticationProvider<
304
320
  session.expiresIn = maxSmallIntegerV8; // maximum expiration length
305
321
  }
306
322
 
307
- if (session != null && session.expiresIn < 60) {
323
+ // `expiresIn < 60` is a PREDICTION that the token is about to lapse, and it is the only trigger the
324
+ // refresh used to have. It cannot see a token the provider has already refused while the backend
325
+ // still believes it is valid — a revoked grant, retired scopes, an app removed from the org — where
326
+ // `expiresIn` stays high and the doomed token is re-sent on every read. `refreshRejectedToken` is
327
+ // the observed counterpart: the caller saw the provider reject this exact token, so refresh it
328
+ // regardless of what its expiry claims.
329
+ if (session != null && (session.expiresIn < 60 || options?.refreshRejectedToken)) {
308
330
  session = await cloudIntegrations.getConnectionSession(
309
331
  this.authProviderId,
310
332
  session.accessToken,
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Per-connection recovery state for a cloud token the provider REFUSED — a revoked grant, retired scopes, an
3
+ * app removed from the org. A purely predictive refresh (`expiresIn < 60`) cannot see that case, because the
4
+ * backend still believes the token is valid, so without this the refused token is re-sent on every read and
5
+ * the failure only clears when the user reconnects by hand.
6
+ *
7
+ * One recovery refresh per connection, then the connection is the failure budget's problem:
8
+ * - {@link recordRejection} on an `AuthenticationError` — arms the refresh, or reports that the rejection is
9
+ * not recoverable so the caller counts it as a real failure instead.
10
+ * - {@link claimRefresh} before the next read resolves its session — spends the rejection, so the refresh
11
+ * fires once and no further read of that connection asks the cloud again.
12
+ *
13
+ * Deliberately NOT re-armed when the refresh hands back a different token. The GK cloud mints a new access
14
+ * token on every `/refresh`, so a credential the provider refuses for a reason a refresh cannot fix (retired
15
+ * scopes being the plain case) would look "rotated" on each attempt: re-arming on that would refresh on every
16
+ * read forever and the failure budget — the thing that disconnects and prompts a reconnect — would never
17
+ * advance. What resets this instead is {@link clear}, called by every path that replaces or removes the stored
18
+ * tokens; a connection that is simply working never reaches here at all.
19
+ */
20
+ export class RejectedTokenTracker {
21
+ private readonly _rejections = new Map<string, 'armed' | 'spent'>();
22
+
23
+ /**
24
+ * Whether a read of `connectionId` must force the cloud refresh of a token known to be refused, spending
25
+ * the rejection so the refresh runs at most once. Spent here rather than after the read because a resolve
26
+ * that throws must not leave the refresh armed and ask the cloud again on the next read.
27
+ */
28
+ claimRefresh(connectionId: string): boolean {
29
+ if (this._rejections.get(connectionId) !== 'armed') return false;
30
+
31
+ this._rejections.set(connectionId, 'spent');
32
+ return true;
33
+ }
34
+
35
+ /**
36
+ * Arms the recovery refresh for `connectionId`. Returns `false` once this connection has had its refresh
37
+ * (or already has one pending), so the caller falls back to its own failure handling: a token still
38
+ * refused after the refresh is not self-healing, and only the failure budget ends in a reconnect prompt.
39
+ */
40
+ recordRejection(connectionId: string): boolean {
41
+ if (this._rejections.has(connectionId)) return false;
42
+
43
+ this._rejections.set(connectionId, 'armed');
44
+ return true;
45
+ }
46
+
47
+ /** Drops all state, for when the stored tokens a rejection could name are replaced or gone. */
48
+ clear(): void {
49
+ this._rejections.clear();
50
+ }
51
+
52
+ /** Whether any connection is holding a rejection. For assertions; the read path uses {@link claimRefresh}. */
53
+ get empty(): boolean {
54
+ return this._rejections.size === 0;
55
+ }
56
+ }
@@ -97,8 +97,12 @@ async function readErrorBodyMessage(response: Response): Promise<{ message: stri
97
97
  message = m;
98
98
  } else if (typeof error === 'string') {
99
99
  message = error;
100
- } else if (error != null && typeof error === 'object' && typeof (error as any).message === 'string') {
101
- message = (error as { message: string }).message;
100
+ } else if (error != null && typeof error === 'object') {
101
+ // Some hosts nest it as `{ error: { message } }`; narrow the same way as the outer object.
102
+ const nested = (error as { message?: unknown }).message;
103
+ if (typeof nested === 'string') {
104
+ message = nested;
105
+ }
102
106
  }
103
107
  }
104
108
  } catch {