@gitkraken/core-gitlens 0.10.1 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -1
- package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts +40 -27
- package/dist/plus/integrations/authentication/configuredIntegrationService.js +92 -22
- package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +10 -6
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +1 -1
- package/dist/plus/integrations/collectionMetadata.js +19 -8
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/errors.d.ts +2 -0
- package/dist/plus/integrations/errors.js +45 -1
- package/dist/plus/integrations/errors.js.map +1 -1
- package/dist/plus/integrations/integrationService.js +2 -1
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +18 -16
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +46 -8
- package/dist/plus/integrations/models/integration.js +159 -54
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.js +2 -6
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +1 -2
- package/dist/plus/integrations/providers/azureDevOps.js +12 -5
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/github.js +1 -4
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
- package/dist/plus/integrations/providers/jira.js +1 -0
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.js +1 -0
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +0 -1
- package/dist/plus/integrations/results.d.ts +8 -5
- package/dist/plus/integrations/results.js +3 -1
- package/dist/plus/integrations/results.js.map +1 -1
- package/package.json +1 -1
|
@@ -165,12 +165,20 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
|
|
|
165
165
|
get connectionExpired(): boolean | undefined;
|
|
166
166
|
protected _session: ProviderAuthenticationSession | null | undefined;
|
|
167
167
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
168
|
+
* Every change to the session (a resolution, a forced re-sync, a disconnect, a reauthentication, a connection
|
|
169
|
+
* switch, a refused token) runs here, one at a time. Each one reads storage and the cloud and then settles
|
|
170
|
+
* `_session`, the stored tokens and the connected flag; run concurrently, one would publish what it read over
|
|
171
|
+
* what another settled meanwhile (gitkraken/kepler#3546). Reads don't queue: they read `_session` as it is.
|
|
171
172
|
*/
|
|
172
|
-
private
|
|
173
|
+
private _sessionTransitions;
|
|
174
|
+
/**
|
|
175
|
+
* Runs `fn` after every transition queued before it. `fn` must not queue another transition and wait for it,
|
|
176
|
+
* which would wait for itself: inside a transition, call the `*Locked` methods instead.
|
|
177
|
+
*/
|
|
178
|
+
protected runSessionTransition<T>(fn: () => Promise<T>): Promise<T>;
|
|
179
|
+
/** An expired session is refreshed first; when that fails, nothing is returned rather than a token the provider refuses. */
|
|
173
180
|
getSession(source: Sources): ProviderAuthenticationSession | Promise<ProviderAuthenticationSession | undefined> | undefined;
|
|
181
|
+
private getRefreshedSession;
|
|
174
182
|
private isSessionForIntegrationHost;
|
|
175
183
|
/**
|
|
176
184
|
* Resolves the session to read as, for a per-connection (multi-account) read. When `connectionId` is
|
|
@@ -188,6 +196,11 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
|
|
|
188
196
|
* limit as `rate-limit`, anything else as `other`.
|
|
189
197
|
*/
|
|
190
198
|
protected resolveReadSession(connectionId: string | undefined, scope: ScopedLogger | undefined, source?: Sources): Promise<ProviderAuthenticationSession | ReadSessionFailure | undefined>;
|
|
199
|
+
/**
|
|
200
|
+
* The session lookups' prelude for a caller that can surface a failure (`throwOnError`): the refresh's failure to
|
|
201
|
+
* report, or `'unconnected'`, so a throttled refresh is not mistaken for "no pull request" and kept.
|
|
202
|
+
*/
|
|
203
|
+
protected prepareSessionLookup(): Promise<Error | 'unconnected' | undefined>;
|
|
191
204
|
/**
|
|
192
205
|
* {@link resolveReadSession} for a read that already rethrows its provider's failures (a repository or account
|
|
193
206
|
* lookup), or whose callers treat any failure as "nothing to show": a token that could not be fetched is
|
|
@@ -201,9 +214,15 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
|
|
|
201
214
|
silent?: boolean;
|
|
202
215
|
currentSessionOnly?: boolean;
|
|
203
216
|
}): Promise<void>;
|
|
217
|
+
private disconnectLocked;
|
|
204
218
|
protected providerOnDisconnect?(): void | Promise<void>;
|
|
205
219
|
reauthenticate(): Promise<void>;
|
|
206
220
|
refresh(): void;
|
|
221
|
+
/**
|
|
222
|
+
* Drops the cached session when its token is no longer `accessToken` (a session replaced outside the
|
|
223
|
+
* integration) and resolves it again, in one transition, so nothing in flight publishes the replaced one.
|
|
224
|
+
*/
|
|
225
|
+
protected resyncSessionIfTokenChanged(accessToken: string | undefined): Promise<void>;
|
|
207
226
|
private _syncRequestsPerFailedUsecase;
|
|
208
227
|
hasSessionSyncRequests(): boolean;
|
|
209
228
|
requestSessionSyncForUsecase(syncReqUsecase: SyncReqUsecase): void;
|
|
@@ -247,10 +266,14 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
|
|
|
247
266
|
* connection changed underneath a warm integration (e.g. after `setPrimaryConnection`/
|
|
248
267
|
* `deleteConnection`). Unlike {@link reset}/{@link disconnect}, it deletes nothing from storage.
|
|
249
268
|
*/
|
|
250
|
-
switchConnection(): void
|
|
251
|
-
private refreshAfterSwitch;
|
|
269
|
+
switchConnection(): Promise<void>;
|
|
252
270
|
private skippedNonCloudReported;
|
|
253
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Resolves the re-sync's failure, when its token fetch threw (cancellations included), so a caller can report it.
|
|
273
|
+
* Never rejects: the cloud sync fans this out over every integration and must not fail on one.
|
|
274
|
+
*/
|
|
275
|
+
syncCloudConnection(state: 'connected' | 'disconnected', forceSync: boolean): Promise<Error | undefined>;
|
|
276
|
+
private syncCloudConnectionLocked;
|
|
254
277
|
/**
|
|
255
278
|
* Proves the credential with an uncached check, rejecting with the provider's `AuthenticationError` when it is
|
|
256
279
|
* refused, and with anything else when the check proves nothing. A `403` proves nothing either: the credential
|
|
@@ -360,13 +383,28 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
|
|
|
360
383
|
silent?: boolean;
|
|
361
384
|
connectionId?: string;
|
|
362
385
|
}): boolean;
|
|
386
|
+
private expireSessionIfToken;
|
|
387
|
+
/** Counts completed cloud re-syncs, so a refresh queued behind one reuses its outcome rather than running another. */
|
|
388
|
+
private _resyncs;
|
|
389
|
+
private _lastResyncFailure;
|
|
363
390
|
private missingExpirityReported;
|
|
364
|
-
|
|
391
|
+
/**
|
|
392
|
+
* Resolves the refresh's failure while the session it could not replace is still unusable, so a read can report
|
|
393
|
+
* that instead of reading with an expired token or as a missing connection.
|
|
394
|
+
*/
|
|
395
|
+
protected refreshSessionIfExpired(): Promise<Error | undefined>;
|
|
365
396
|
trackRequestException(options?: {
|
|
366
397
|
silent?: boolean;
|
|
367
398
|
}): void;
|
|
368
399
|
isConnected(): Promise<boolean>;
|
|
369
400
|
private ensureSession;
|
|
401
|
+
/**
|
|
402
|
+
* Asks the auth provider for a session interactively, storing what it gets; whether one came back. A declined
|
|
403
|
+
* consent or any other failure is not one, and never throws: it is resolved, like any read, from storage.
|
|
404
|
+
*/
|
|
405
|
+
private signIn;
|
|
406
|
+
/** Resolves and publishes the session. Only ever runs inside a transition (see {@link runSessionTransition}). */
|
|
407
|
+
private resolveSessionLocked;
|
|
370
408
|
getIgnoreSSLErrors(): boolean | 'force';
|
|
371
409
|
searchMyIssues(resource?: ResourceDescriptor, cancellation?: AbortSignal, connectionId?: string): Promise<IssueShape[] | undefined>;
|
|
372
410
|
searchMyIssues(resources?: ResourceDescriptor[], cancellation?: AbortSignal, connectionId?: string): Promise<IssueShape[] | undefined>;
|
|
@@ -8,7 +8,7 @@ import { AuthenticationError, RequestClientError, getDeferredRequestFailure } fr
|
|
|
8
8
|
import { Emitter } from "../../../utils/event.js";
|
|
9
9
|
import { fnv1aHash64 } from "../../../utils/hash.js";
|
|
10
10
|
import { gate } from "../../../utils/decorators/gate.js";
|
|
11
|
-
import { toError } from "../errors.js";
|
|
11
|
+
import { isProviderUnreachableError, toError } from "../errors.js";
|
|
12
12
|
import "../constants.js";
|
|
13
13
|
import { isAzureProviderId } from "../providers/providerErrors.js";
|
|
14
14
|
import { attributeScopedAuthFailures, hasOnlyScopedAuthFailures, markCredentialRefusals, toCollectionScopeFailure } from "../collectionMetadata.js";
|
|
@@ -144,17 +144,34 @@ var IntegrationBase = class IntegrationBase {
|
|
|
144
144
|
}
|
|
145
145
|
_session;
|
|
146
146
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
147
|
+
* Every change to the session (a resolution, a forced re-sync, a disconnect, a reauthentication, a connection
|
|
148
|
+
* switch, a refused token) runs here, one at a time. Each one reads storage and the cloud and then settles
|
|
149
|
+
* `_session`, the stored tokens and the connected flag; run concurrently, one would publish what it read over
|
|
150
|
+
* what another settled meanwhile (gitkraken/kepler#3546). Reads don't queue: they read `_session` as it is.
|
|
150
151
|
*/
|
|
151
|
-
|
|
152
|
+
_sessionTransitions = Promise.resolve();
|
|
153
|
+
/**
|
|
154
|
+
* Runs `fn` after every transition queued before it. `fn` must not queue another transition and wait for it,
|
|
155
|
+
* which would wait for itself: inside a transition, call the `*Locked` methods instead.
|
|
156
|
+
*/
|
|
157
|
+
runSessionTransition(fn) {
|
|
158
|
+
const run = this._sessionTransitions.then(fn);
|
|
159
|
+
this._sessionTransitions = run.then(() => void 0, () => void 0);
|
|
160
|
+
return run;
|
|
161
|
+
}
|
|
162
|
+
/** An expired session is refreshed first; when that fails, nothing is returned rather than a token the provider refuses. */
|
|
152
163
|
getSession(source) {
|
|
153
164
|
if (this._session === void 0) return this.ensureSession({
|
|
154
165
|
createIfNeeded: false,
|
|
155
166
|
source
|
|
156
167
|
});
|
|
157
|
-
|
|
168
|
+
if (this._session == null || !this.isSessionForIntegrationHost(this._session)) return void 0;
|
|
169
|
+
if (this.connectionExpired) return this.getRefreshedSession();
|
|
170
|
+
return this._session;
|
|
171
|
+
}
|
|
172
|
+
async getRefreshedSession() {
|
|
173
|
+
if (await this.refreshSessionIfExpired() != null) return void 0;
|
|
174
|
+
return this._session != null && this.connectionExpired !== true && this.isSessionForIntegrationHost(this._session) ? this._session : void 0;
|
|
158
175
|
}
|
|
159
176
|
isSessionForIntegrationHost(session) {
|
|
160
177
|
if (!isSelfManagedHostIntegrationId(this.id)) return true;
|
|
@@ -196,10 +213,21 @@ var IntegrationBase = class IntegrationBase {
|
|
|
196
213
|
}
|
|
197
214
|
}
|
|
198
215
|
if (!(this.maybeConnected ?? await this.isConnected())) return void 0;
|
|
199
|
-
await this.refreshSessionIfExpired(
|
|
216
|
+
const refreshFailure = await this.refreshSessionIfExpired();
|
|
217
|
+
if (refreshFailure != null) return new ReadSessionFailure(refreshFailure);
|
|
200
218
|
return this._session != null && this.isSessionForIntegrationHost(this._session) ? this._session : void 0;
|
|
201
219
|
}
|
|
202
220
|
/**
|
|
221
|
+
* The session lookups' prelude for a caller that can surface a failure (`throwOnError`): the refresh's failure to
|
|
222
|
+
* report, or `'unconnected'`, so a throttled refresh is not mistaken for "no pull request" and kept.
|
|
223
|
+
*/
|
|
224
|
+
async prepareSessionLookup() {
|
|
225
|
+
if (!(this.maybeConnected ?? await this.isConnected())) return "unconnected";
|
|
226
|
+
const failure = await this.refreshSessionIfExpired();
|
|
227
|
+
if (failure != null) return failure;
|
|
228
|
+
return this._session != null ? void 0 : "unconnected";
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
203
231
|
* {@link resolveReadSession} for a read that already rethrows its provider's failures (a repository or account
|
|
204
232
|
* lookup), or whose callers treat any failure as "nothing to show": a token that could not be fetched is
|
|
205
233
|
* thrown like those failures, so it is classified as the failed request it is rather than as a missing
|
|
@@ -222,9 +250,8 @@ var IntegrationBase = class IntegrationBase {
|
|
|
222
250
|
}
|
|
223
251
|
async disconnect(options) {
|
|
224
252
|
if (options?.currentSessionOnly && this._session === null) return;
|
|
225
|
-
const connected = this._session != null;
|
|
226
253
|
let signOut = !options?.currentSessionOnly;
|
|
227
|
-
if (
|
|
254
|
+
if (this._session != null && !options?.currentSessionOnly && !options?.silent) {
|
|
228
255
|
const decision = await this.ctx.hooks?.onConfirmDisconnect?.({
|
|
229
256
|
integrationName: this.name,
|
|
230
257
|
offerSignOut: this.authenticationService.supports(this.authProvider.id)
|
|
@@ -232,6 +259,11 @@ var IntegrationBase = class IntegrationBase {
|
|
|
232
259
|
if (decision == null) return;
|
|
233
260
|
signOut = decision.signOut;
|
|
234
261
|
}
|
|
262
|
+
await this.runSessionTransition(() => this.disconnectLocked(options, signOut));
|
|
263
|
+
}
|
|
264
|
+
async disconnectLocked(options, signOut) {
|
|
265
|
+
if (options?.currentSessionOnly && this._session === null) return;
|
|
266
|
+
const connected = this._session != null;
|
|
235
267
|
if (signOut) await (await this.authenticationService.get(this.authProvider.id)).deleteAllSessions(this.authProviderDescriptor);
|
|
236
268
|
this.resetRequestExceptionCount("all");
|
|
237
269
|
this._session = null;
|
|
@@ -248,8 +280,11 @@ var IntegrationBase = class IntegrationBase {
|
|
|
248
280
|
}
|
|
249
281
|
async reauthenticate() {
|
|
250
282
|
this.onStoredTokensReplaced();
|
|
251
|
-
if (this.
|
|
252
|
-
|
|
283
|
+
if (!await this.runSessionTransition(() => {
|
|
284
|
+
if (this._session === void 0) return Promise.resolve(false);
|
|
285
|
+
this._session = void 0;
|
|
286
|
+
return Promise.resolve(true);
|
|
287
|
+
})) return;
|
|
253
288
|
await this.ensureSession({
|
|
254
289
|
createIfNeeded: true,
|
|
255
290
|
forceNewSession: true
|
|
@@ -258,6 +293,16 @@ var IntegrationBase = class IntegrationBase {
|
|
|
258
293
|
refresh() {
|
|
259
294
|
this.ensureSession({ createIfNeeded: false });
|
|
260
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Drops the cached session when its token is no longer `accessToken` (a session replaced outside the
|
|
298
|
+
* integration) and resolves it again, in one transition, so nothing in flight publishes the replaced one.
|
|
299
|
+
*/
|
|
300
|
+
resyncSessionIfTokenChanged(accessToken) {
|
|
301
|
+
return this.runSessionTransition(async () => {
|
|
302
|
+
if (this._session != null && this._session.accessToken !== accessToken) this._session = void 0;
|
|
303
|
+
await this.resolveSessionLocked({ createIfNeeded: false });
|
|
304
|
+
});
|
|
305
|
+
}
|
|
261
306
|
_syncRequestsPerFailedUsecase = /* @__PURE__ */ new Set();
|
|
262
307
|
hasSessionSyncRequests() {
|
|
263
308
|
return this._syncRequestsPerFailedUsecase.size > 0;
|
|
@@ -311,8 +356,10 @@ var IntegrationBase = class IntegrationBase {
|
|
|
311
356
|
this.requestExceptionCount = 0;
|
|
312
357
|
}
|
|
313
358
|
async reset() {
|
|
314
|
-
await this.
|
|
315
|
-
|
|
359
|
+
await this.runSessionTransition(async () => {
|
|
360
|
+
await this.disconnectLocked({ silent: true }, true);
|
|
361
|
+
await this.ctx.storage.deleteWorkspace(this.connectedKey);
|
|
362
|
+
});
|
|
316
363
|
}
|
|
317
364
|
/**
|
|
318
365
|
* Drops the in-memory session so the next access re-resolves it from storage. Used when the primary
|
|
@@ -321,24 +368,30 @@ var IntegrationBase = class IntegrationBase {
|
|
|
321
368
|
*/
|
|
322
369
|
switchConnection() {
|
|
323
370
|
this.onStoredTokensReplaced();
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
371
|
+
return this.runSessionTransition(async () => {
|
|
372
|
+
if (this._session === void 0) return;
|
|
373
|
+
const wasConnected = this._session != null;
|
|
374
|
+
this._session = void 0;
|
|
375
|
+
this._onDidChange.fire();
|
|
376
|
+
if (await this.resolveSessionLocked({ createIfNeeded: false }) != null || !wasConnected) return;
|
|
377
|
+
this._onDidChange.fire();
|
|
378
|
+
this.didChangeConnection?.fire({
|
|
379
|
+
integration: this,
|
|
380
|
+
key: this.key,
|
|
381
|
+
reason: "disconnected"
|
|
382
|
+
});
|
|
383
|
+
await this.providerOnDisconnect?.();
|
|
337
384
|
});
|
|
338
|
-
await this.providerOnDisconnect?.();
|
|
339
385
|
}
|
|
340
386
|
skippedNonCloudReported = false;
|
|
341
|
-
|
|
387
|
+
/**
|
|
388
|
+
* Resolves the re-sync's failure, when its token fetch threw (cancellations included), so a caller can report it.
|
|
389
|
+
* Never rejects: the cloud sync fans this out over every integration and must not fail on one.
|
|
390
|
+
*/
|
|
391
|
+
syncCloudConnection(state, forceSync) {
|
|
392
|
+
return this.runSessionTransition(() => this.syncCloudConnectionLocked(state, forceSync));
|
|
393
|
+
}
|
|
394
|
+
async syncCloudConnectionLocked(state, forceSync) {
|
|
342
395
|
const scope = getScopedLogger();
|
|
343
396
|
if (this._session?.cloud === false && state !== "connected") {
|
|
344
397
|
if (this.id !== "github" && !this.skippedNonCloudReported) {
|
|
@@ -351,13 +404,13 @@ var IntegrationBase = class IntegrationBase {
|
|
|
351
404
|
}
|
|
352
405
|
return;
|
|
353
406
|
}
|
|
407
|
+
let failure;
|
|
354
408
|
switch (state) {
|
|
355
409
|
case "connected": {
|
|
356
410
|
const oldSession = this._session;
|
|
357
411
|
let resyncing = false;
|
|
358
412
|
if (forceSync) {
|
|
359
413
|
this.onStoredTokensReplaced();
|
|
360
|
-
this._session = void 0;
|
|
361
414
|
await this.ctx.storage.deleteWorkspace(this.connectedKey);
|
|
362
415
|
resyncing = true;
|
|
363
416
|
} else {
|
|
@@ -367,20 +420,24 @@ var IntegrationBase = class IntegrationBase {
|
|
|
367
420
|
let newSession;
|
|
368
421
|
let refetchFailed = false;
|
|
369
422
|
try {
|
|
370
|
-
newSession = await this.
|
|
423
|
+
newSession = await this.resolveSessionLocked({
|
|
371
424
|
sync: forceSync,
|
|
372
425
|
refetch: resyncing
|
|
373
426
|
});
|
|
374
427
|
} catch (ex) {
|
|
375
|
-
if (!isCancellationError(ex)) scope?.error(ex);
|
|
376
428
|
refetchFailed = true;
|
|
429
|
+
if (!isCancellationError(ex)) scope?.error(ex);
|
|
430
|
+
failure = toError(ex);
|
|
377
431
|
}
|
|
378
432
|
if (oldSession && newSession && newSession.accessToken !== oldSession.accessToken) this.resetRequestExceptionCount("all");
|
|
379
433
|
if (resyncing && newSession == null && !refetchFailed) await (await this.authenticationService.get(this.authProvider.id)).deleteSession(this.authProviderDescriptor, { preserveConfigured: false });
|
|
380
434
|
break;
|
|
381
435
|
}
|
|
382
|
-
case "disconnected": await this.
|
|
436
|
+
case "disconnected": await this.disconnectLocked({ silent: true }, true);
|
|
383
437
|
}
|
|
438
|
+
this._resyncs++;
|
|
439
|
+
this._lastResyncFailure = failure;
|
|
440
|
+
return failure;
|
|
384
441
|
}
|
|
385
442
|
/**
|
|
386
443
|
* Credentials that passed {@link validateCredential} within the last minute. A probe already in flight is
|
|
@@ -544,24 +601,48 @@ var IntegrationBase = class IntegrationBase {
|
|
|
544
601
|
}
|
|
545
602
|
if (ex instanceof AuthenticationError && this._session?.cloud && !this.hasSessionSyncRequests()) {
|
|
546
603
|
this.requestSessionSyncForUsecase(syncReqUsecase);
|
|
547
|
-
this._session
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
604
|
+
const refused = this._session.accessToken;
|
|
605
|
+
this.expireSessionIfToken(refused);
|
|
606
|
+
this.runSessionTransition(() => {
|
|
607
|
+
this.expireSessionIfToken(refused);
|
|
608
|
+
return Promise.resolve();
|
|
609
|
+
});
|
|
551
610
|
return false;
|
|
552
611
|
}
|
|
553
612
|
if (!(ex instanceof AuthenticationError) && !(ex instanceof RequestClientError)) return false;
|
|
554
613
|
this.trackRequestException(options);
|
|
555
614
|
return true;
|
|
556
615
|
}
|
|
616
|
+
expireSessionIfToken(accessToken) {
|
|
617
|
+
if (this._session?.accessToken !== accessToken || this.connectionExpired === true) return;
|
|
618
|
+
this._session = {
|
|
619
|
+
...this._session,
|
|
620
|
+
expiresAt: /* @__PURE__ */ new Date(Date.now() - 1)
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/** Counts completed cloud re-syncs, so a refresh queued behind one reuses its outcome rather than running another. */
|
|
624
|
+
_resyncs = 0;
|
|
625
|
+
_lastResyncFailure;
|
|
557
626
|
missingExpirityReported = false;
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
627
|
+
/**
|
|
628
|
+
* Resolves the refresh's failure while the session it could not replace is still unusable, so a read can report
|
|
629
|
+
* that instead of reading with an expired token or as a missing connection.
|
|
630
|
+
*/
|
|
631
|
+
async refreshSessionIfExpired() {
|
|
632
|
+
if (this._session?.expiresAt != null && this._session.expiresAt < /* @__PURE__ */ new Date()) {
|
|
633
|
+
const resyncs = this._resyncs;
|
|
634
|
+
return this.runSessionTransition(async () => {
|
|
635
|
+
let failure;
|
|
636
|
+
if (this._resyncs !== resyncs) failure = this._lastResyncFailure;
|
|
637
|
+
else if (this._session != null && this.connectionExpired === true) try {
|
|
638
|
+
failure = await this.syncCloudConnectionLocked("connected", true);
|
|
639
|
+
} catch (ex) {
|
|
640
|
+
failure = toError(ex);
|
|
641
|
+
}
|
|
642
|
+
if (this._session == null || this.connectionExpired !== true) return void 0;
|
|
643
|
+
return failure ?? /* @__PURE__ */ new Error(`The ${this.name} session could not be refreshed`);
|
|
644
|
+
});
|
|
645
|
+
} else if (this._session?.expiresAt == null && this.id !== "github" && !this.missingExpirityReported) {
|
|
565
646
|
this.ctx.hooks?.session?.onRefreshSkipped?.({
|
|
566
647
|
id: this.id,
|
|
567
648
|
reason: "missing-expiry",
|
|
@@ -578,11 +659,36 @@ var IntegrationBase = class IntegrationBase {
|
|
|
578
659
|
}
|
|
579
660
|
}
|
|
580
661
|
async isConnected() {
|
|
581
|
-
|
|
662
|
+
const session = this._session === void 0 ? await this.ensureSession({
|
|
663
|
+
createIfNeeded: false,
|
|
664
|
+
source: "integrations"
|
|
665
|
+
}) : this._session;
|
|
666
|
+
return session != null && this.isSessionForIntegrationHost(session);
|
|
582
667
|
}
|
|
583
668
|
async ensureSession(options) {
|
|
669
|
+
if (options.createIfNeeded || options.forceNewSession) {
|
|
670
|
+
if (!await this.signIn(options)) return this._session ?? void 0;
|
|
671
|
+
return this.runSessionTransition(() => this.resolveSessionLocked({ createIfNeeded: false }));
|
|
672
|
+
}
|
|
673
|
+
return this.runSessionTransition(() => this.resolveSessionLocked(options));
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Asks the auth provider for a session interactively, storing what it gets; whether one came back. A declined
|
|
677
|
+
* consent or any other failure is not one, and never throws: it is resolved, like any read, from storage.
|
|
678
|
+
*/
|
|
679
|
+
async signIn(options) {
|
|
680
|
+
if (this.ctx.config.isIntegrationsEnabled?.() === false) return false;
|
|
681
|
+
await this.ctx.storage.deleteWorkspace(this.connectedKey);
|
|
682
|
+
try {
|
|
683
|
+
return await (await this.authenticationService.get(this.authProvider.id)).getSession(this.authProviderDescriptor, options) != null;
|
|
684
|
+
} catch {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
/** Resolves and publishes the session. Only ever runs inside a transition (see {@link runSessionTransition}). */
|
|
689
|
+
async resolveSessionLocked(options) {
|
|
584
690
|
const scope = getScopedLogger();
|
|
585
|
-
const { createIfNeeded,
|
|
691
|
+
const { createIfNeeded, source, sync, refetch } = options;
|
|
586
692
|
if (this._session != null && !refetch) {
|
|
587
693
|
if (this.isSessionForIntegrationHost(this._session)) return this._session;
|
|
588
694
|
this._session = null;
|
|
@@ -590,7 +696,6 @@ var IntegrationBase = class IntegrationBase {
|
|
|
590
696
|
if (this.ctx.config.isIntegrationsEnabled?.() === false) return void 0;
|
|
591
697
|
if (createIfNeeded || sync) await this.ctx.storage.deleteWorkspace(this.connectedKey);
|
|
592
698
|
else if (this.ctx.storage.getWorkspace(this.connectedKey) === false) return;
|
|
593
|
-
const generation = this._sessionGeneration;
|
|
594
699
|
let session;
|
|
595
700
|
try {
|
|
596
701
|
session = await (await this.authenticationService.get(this.authProvider.id)).getSession(this.authProviderDescriptor, sync ? {
|
|
@@ -599,7 +704,6 @@ var IntegrationBase = class IntegrationBase {
|
|
|
599
704
|
source
|
|
600
705
|
} : {
|
|
601
706
|
createIfNeeded,
|
|
602
|
-
forceNewSession,
|
|
603
707
|
source
|
|
604
708
|
});
|
|
605
709
|
if (session?.expiresAt != null && session.expiresAt < /* @__PURE__ */ new Date()) session = null;
|
|
@@ -613,8 +717,6 @@ var IntegrationBase = class IntegrationBase {
|
|
|
613
717
|
}
|
|
614
718
|
session = null;
|
|
615
719
|
}
|
|
616
|
-
if (refetch) this._sessionGeneration++;
|
|
617
|
-
else if (generation !== this._sessionGeneration) return this._session ?? void 0;
|
|
618
720
|
if (session === void 0 && !createIfNeeded && !sync) await this.ctx.storage.deleteWorkspace(this.connectedKey);
|
|
619
721
|
this._session = session ?? null;
|
|
620
722
|
this.smoothifyRequestExceptionCount();
|
|
@@ -707,8 +809,7 @@ var IntegrationBase = class IntegrationBase {
|
|
|
707
809
|
}
|
|
708
810
|
async getLinkedIssueOrPullRequest(resource, link, options) {
|
|
709
811
|
const scope = getScopedLogger();
|
|
710
|
-
if (
|
|
711
|
-
await this.refreshSessionIfExpired(scope);
|
|
812
|
+
if (await this.prepareSessionLookup() != null) return void 0;
|
|
712
813
|
return this.ctx.cache.getIssueOrPullRequest(link.key, options?.type, resource, this, () => ({ value: (async () => {
|
|
713
814
|
try {
|
|
714
815
|
const result = await this.getProviderLinkedIssueOrPullRequest(this._session, resource, link, options?.type);
|
|
@@ -774,7 +875,7 @@ var IntegrationBase = class IntegrationBase {
|
|
|
774
875
|
this.resetRequestExceptionCount("getCurrentAccount");
|
|
775
876
|
return account;
|
|
776
877
|
} catch (ex) {
|
|
777
|
-
if (isCancellationError(ex)) {
|
|
878
|
+
if (isCancellationError(ex) && !isProviderUnreachableError(ex)) {
|
|
778
879
|
cacheable.invalidate();
|
|
779
880
|
return;
|
|
780
881
|
}
|
|
@@ -806,8 +907,12 @@ var IntegrationBase = class IntegrationBase {
|
|
|
806
907
|
}
|
|
807
908
|
async getPullRequest(resource, id, options) {
|
|
808
909
|
const scope = getScopedLogger();
|
|
809
|
-
|
|
810
|
-
|
|
910
|
+
const refreshFailure = await this.prepareSessionLookup();
|
|
911
|
+
if (refreshFailure === "unconnected") return void 0;
|
|
912
|
+
if (refreshFailure != null) {
|
|
913
|
+
if (options?.throwOnError) throw refreshFailure;
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
811
916
|
return await this.ctx.cache.getPullRequest(id, resource, this, (cacheable) => ({ value: (async () => {
|
|
812
917
|
try {
|
|
813
918
|
const result = await this.getProviderPullRequest?.(this._session, resource, id);
|