@gitkraken/core-gitlens 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -1
- package/dist/plus/integrations/authentication/cloudIntegrationService.js +24 -7
- package/dist/plus/integrations/authentication/cloudIntegrationService.js.map +1 -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.d.ts +7 -0
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +22 -8
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
- package/dist/plus/integrations/integrationService.js +6 -3
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +38 -36
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +71 -7
- package/dist/plus/integrations/models/integration.js +201 -55
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issuesIntegration.js +9 -9
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.js +1 -1
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.js +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.js +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.js +1 -1
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/github.js +3 -6
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.js +1 -1
- package/dist/plus/integrations/providers/gitlab.js.map +1 -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/package.json +1 -1
|
@@ -37,6 +37,12 @@ export type IntegrationResult<T> = {
|
|
|
37
37
|
duration?: number;
|
|
38
38
|
value?: never;
|
|
39
39
|
} | undefined;
|
|
40
|
+
/** A per-connection read's session that could not be resolved for a retryable reason (see `resolveReadSession`). */
|
|
41
|
+
export declare class ReadSessionFailure {
|
|
42
|
+
readonly error: Error;
|
|
43
|
+
constructor(error: Error);
|
|
44
|
+
}
|
|
45
|
+
export declare function isReadSessionFailure(session: ProviderAuthenticationSession | ReadSessionFailure | undefined): session is ReadSessionFailure;
|
|
40
46
|
/**
|
|
41
47
|
* One target's answer in a batch read. A slot rejected by an authentication refusal carries the scope failure it was
|
|
42
48
|
* recorded as (see `IntegrationBase.settleBatchRefusals`).
|
|
@@ -158,26 +164,65 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
|
|
|
158
164
|
protected getSessionFingerprint(session: ProviderAuthenticationSession): string;
|
|
159
165
|
get connectionExpired(): boolean | undefined;
|
|
160
166
|
protected _session: ProviderAuthenticationSession | null | undefined;
|
|
167
|
+
/**
|
|
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.
|
|
172
|
+
*/
|
|
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. */
|
|
161
180
|
getSession(source: Sources): ProviderAuthenticationSession | Promise<ProviderAuthenticationSession | undefined> | undefined;
|
|
181
|
+
private getRefreshedSession;
|
|
162
182
|
private isSessionForIntegrationHost;
|
|
163
183
|
/**
|
|
164
184
|
* Resolves the session to read as, for a per-connection (multi-account) read. When `connectionId` is
|
|
165
185
|
* omitted this is the integration's primary session, resolved exactly like the existing read flow
|
|
166
186
|
* (ensure-connected + refresh-if-expired). When set, it resolves THAT connection's session directly
|
|
167
187
|
* from the auth provider — refreshing it if expired — WITHOUT disturbing the cached primary
|
|
168
|
-
* `_session`.
|
|
169
|
-
*
|
|
188
|
+
* `_session`. Never throws: `undefined` when there is no session to read with (the connection is gone,
|
|
189
|
+
* the provider isn't connected), `{ error }` when the connection's token could not be fetched.
|
|
190
|
+
*
|
|
191
|
+
* The two must not be conflated. A token fetch that THROWS is, by `getConnectionSession`'s contract, a
|
|
192
|
+
* retryable failure (a rate-limited or failing GK API, a network error), not evidence that the connection is
|
|
193
|
+
* gone (#5569). Answering "no session" for it made every connection read during a GK API 429 report
|
|
194
|
+
* `no-connection`, which a consumer rightly treats as a credential to reconnect (gitkraken/kepler#3546).
|
|
195
|
+
* Returned as the read's error instead, the read core surfaces it like any other failed request: a rate
|
|
196
|
+
* limit as `rate-limit`, anything else as `other`.
|
|
197
|
+
*/
|
|
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>;
|
|
204
|
+
/**
|
|
205
|
+
* {@link resolveReadSession} for a read that already rethrows its provider's failures (a repository or account
|
|
206
|
+
* lookup), or whose callers treat any failure as "nothing to show": a token that could not be fetched is
|
|
207
|
+
* thrown like those failures, so it is classified as the failed request it is rather than as a missing
|
|
208
|
+
* connection.
|
|
170
209
|
*/
|
|
171
|
-
protected
|
|
210
|
+
protected resolveReadSessionOrThrow(connectionId: string | undefined, scope: ScopedLogger | undefined): Promise<ProviderAuthenticationSession | undefined>;
|
|
172
211
|
connect(source: Sources): Promise<boolean>;
|
|
173
212
|
protected providerOnConnect?(): void | Promise<void>;
|
|
174
213
|
disconnect(options?: {
|
|
175
214
|
silent?: boolean;
|
|
176
215
|
currentSessionOnly?: boolean;
|
|
177
216
|
}): Promise<void>;
|
|
217
|
+
private disconnectLocked;
|
|
178
218
|
protected providerOnDisconnect?(): void | Promise<void>;
|
|
179
219
|
reauthenticate(): Promise<void>;
|
|
180
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>;
|
|
181
226
|
private _syncRequestsPerFailedUsecase;
|
|
182
227
|
hasSessionSyncRequests(): boolean;
|
|
183
228
|
requestSessionSyncForUsecase(syncReqUsecase: SyncReqUsecase): void;
|
|
@@ -221,10 +266,14 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
|
|
|
221
266
|
* connection changed underneath a warm integration (e.g. after `setPrimaryConnection`/
|
|
222
267
|
* `deleteConnection`). Unlike {@link reset}/{@link disconnect}, it deletes nothing from storage.
|
|
223
268
|
*/
|
|
224
|
-
switchConnection(): void
|
|
225
|
-
private refreshAfterSwitch;
|
|
269
|
+
switchConnection(): Promise<void>;
|
|
226
270
|
private skippedNonCloudReported;
|
|
227
|
-
|
|
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;
|
|
228
277
|
/**
|
|
229
278
|
* Proves the credential with an uncached check, rejecting with the provider's `AuthenticationError` when it is
|
|
230
279
|
* refused, and with anything else when the check proves nothing. A `403` proves nothing either: the credential
|
|
@@ -334,13 +383,28 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
|
|
|
334
383
|
silent?: boolean;
|
|
335
384
|
connectionId?: string;
|
|
336
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;
|
|
337
390
|
private missingExpirityReported;
|
|
338
|
-
|
|
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>;
|
|
339
396
|
trackRequestException(options?: {
|
|
340
397
|
silent?: boolean;
|
|
341
398
|
}): void;
|
|
342
399
|
isConnected(): Promise<boolean>;
|
|
343
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;
|
|
344
408
|
getIgnoreSSLErrors(): boolean | 'force';
|
|
345
409
|
searchMyIssues(resource?: ResourceDescriptor, cancellation?: AbortSignal, connectionId?: string): Promise<IssueShape[] | undefined>;
|
|
346
410
|
searchMyIssues(resources?: ResourceDescriptor[], cancellation?: AbortSignal, connectionId?: string): Promise<IssueShape[] | undefined>;
|
|
@@ -19,6 +19,16 @@ import { providersMetadata } from "../providers/models.js";
|
|
|
19
19
|
import { RejectedTokenTracker } from "../authentication/rejectedTokenTracker.js";
|
|
20
20
|
import { getCachedIssue } from "./issueCache.js";
|
|
21
21
|
//#region ../plus/integrations/src/models/integration.ts
|
|
22
|
+
/** A per-connection read's session that could not be resolved for a retryable reason (see `resolveReadSession`). */
|
|
23
|
+
var ReadSessionFailure = class {
|
|
24
|
+
error;
|
|
25
|
+
constructor(error) {
|
|
26
|
+
this.error = error;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
function isReadSessionFailure(session) {
|
|
30
|
+
return session instanceof ReadSessionFailure;
|
|
31
|
+
}
|
|
22
32
|
/** Every {@link PullRequestEtagInclude}, in the canonical order an etag lists them in. */
|
|
23
33
|
const pullRequestEtagIncludes = [
|
|
24
34
|
"mergeable",
|
|
@@ -133,12 +143,35 @@ var IntegrationBase = class IntegrationBase {
|
|
|
133
143
|
return new Date(this._session.expiresAt) < /* @__PURE__ */ new Date();
|
|
134
144
|
}
|
|
135
145
|
_session;
|
|
146
|
+
/**
|
|
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.
|
|
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. */
|
|
136
163
|
getSession(source) {
|
|
137
164
|
if (this._session === void 0) return this.ensureSession({
|
|
138
165
|
createIfNeeded: false,
|
|
139
166
|
source
|
|
140
167
|
});
|
|
141
|
-
|
|
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;
|
|
142
175
|
}
|
|
143
176
|
isSessionForIntegrationHost(session) {
|
|
144
177
|
if (!isSelfManagedHostIntegrationId(this.id)) return true;
|
|
@@ -149,8 +182,15 @@ var IntegrationBase = class IntegrationBase {
|
|
|
149
182
|
* omitted this is the integration's primary session, resolved exactly like the existing read flow
|
|
150
183
|
* (ensure-connected + refresh-if-expired). When set, it resolves THAT connection's session directly
|
|
151
184
|
* from the auth provider — refreshing it if expired — WITHOUT disturbing the cached primary
|
|
152
|
-
* `_session`.
|
|
153
|
-
*
|
|
185
|
+
* `_session`. Never throws: `undefined` when there is no session to read with (the connection is gone,
|
|
186
|
+
* the provider isn't connected), `{ error }` when the connection's token could not be fetched.
|
|
187
|
+
*
|
|
188
|
+
* The two must not be conflated. A token fetch that THROWS is, by `getConnectionSession`'s contract, a
|
|
189
|
+
* retryable failure (a rate-limited or failing GK API, a network error), not evidence that the connection is
|
|
190
|
+
* gone (#5569). Answering "no session" for it made every connection read during a GK API 429 report
|
|
191
|
+
* `no-connection`, which a consumer rightly treats as a credential to reconnect (gitkraken/kepler#3546).
|
|
192
|
+
* Returned as the read's error instead, the read core surfaces it like any other failed request: a rate
|
|
193
|
+
* limit as `rate-limit`, anything else as `other`.
|
|
154
194
|
*/
|
|
155
195
|
async resolveReadSession(connectionId, scope, source) {
|
|
156
196
|
if (this.ctx.config.isIntegrationsEnabled?.() === false || this.ctx.storage.getWorkspace(this.connectedKey) === false) return;
|
|
@@ -167,14 +207,37 @@ var IntegrationBase = class IntegrationBase {
|
|
|
167
207
|
});
|
|
168
208
|
return session != null && this.isSessionForIntegrationHost(session) ? session : void 0;
|
|
169
209
|
} catch (ex) {
|
|
210
|
+
if (isCancellationError(ex)) return void 0;
|
|
170
211
|
scope?.error(ex);
|
|
171
|
-
return;
|
|
212
|
+
return new ReadSessionFailure(toError(ex));
|
|
172
213
|
}
|
|
173
214
|
}
|
|
174
215
|
if (!(this.maybeConnected ?? await this.isConnected())) return void 0;
|
|
175
|
-
await this.refreshSessionIfExpired(
|
|
216
|
+
const refreshFailure = await this.refreshSessionIfExpired();
|
|
217
|
+
if (refreshFailure != null) return new ReadSessionFailure(refreshFailure);
|
|
176
218
|
return this._session != null && this.isSessionForIntegrationHost(this._session) ? this._session : void 0;
|
|
177
219
|
}
|
|
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
|
+
/**
|
|
231
|
+
* {@link resolveReadSession} for a read that already rethrows its provider's failures (a repository or account
|
|
232
|
+
* lookup), or whose callers treat any failure as "nothing to show": a token that could not be fetched is
|
|
233
|
+
* thrown like those failures, so it is classified as the failed request it is rather than as a missing
|
|
234
|
+
* connection.
|
|
235
|
+
*/
|
|
236
|
+
async resolveReadSessionOrThrow(connectionId, scope) {
|
|
237
|
+
const session = await this.resolveReadSession(connectionId, scope);
|
|
238
|
+
if (isReadSessionFailure(session)) throw session.error;
|
|
239
|
+
return session;
|
|
240
|
+
}
|
|
178
241
|
async connect(source) {
|
|
179
242
|
try {
|
|
180
243
|
return Boolean(await this.ensureSession({
|
|
@@ -187,9 +250,8 @@ var IntegrationBase = class IntegrationBase {
|
|
|
187
250
|
}
|
|
188
251
|
async disconnect(options) {
|
|
189
252
|
if (options?.currentSessionOnly && this._session === null) return;
|
|
190
|
-
const connected = this._session != null;
|
|
191
253
|
let signOut = !options?.currentSessionOnly;
|
|
192
|
-
if (
|
|
254
|
+
if (this._session != null && !options?.currentSessionOnly && !options?.silent) {
|
|
193
255
|
const decision = await this.ctx.hooks?.onConfirmDisconnect?.({
|
|
194
256
|
integrationName: this.name,
|
|
195
257
|
offerSignOut: this.authenticationService.supports(this.authProvider.id)
|
|
@@ -197,6 +259,11 @@ var IntegrationBase = class IntegrationBase {
|
|
|
197
259
|
if (decision == null) return;
|
|
198
260
|
signOut = decision.signOut;
|
|
199
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;
|
|
200
267
|
if (signOut) await (await this.authenticationService.get(this.authProvider.id)).deleteAllSessions(this.authProviderDescriptor);
|
|
201
268
|
this.resetRequestExceptionCount("all");
|
|
202
269
|
this._session = null;
|
|
@@ -213,8 +280,11 @@ var IntegrationBase = class IntegrationBase {
|
|
|
213
280
|
}
|
|
214
281
|
async reauthenticate() {
|
|
215
282
|
this.onStoredTokensReplaced();
|
|
216
|
-
if (this.
|
|
217
|
-
|
|
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;
|
|
218
288
|
await this.ensureSession({
|
|
219
289
|
createIfNeeded: true,
|
|
220
290
|
forceNewSession: true
|
|
@@ -223,6 +293,16 @@ var IntegrationBase = class IntegrationBase {
|
|
|
223
293
|
refresh() {
|
|
224
294
|
this.ensureSession({ createIfNeeded: false });
|
|
225
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
|
+
}
|
|
226
306
|
_syncRequestsPerFailedUsecase = /* @__PURE__ */ new Set();
|
|
227
307
|
hasSessionSyncRequests() {
|
|
228
308
|
return this._syncRequestsPerFailedUsecase.size > 0;
|
|
@@ -276,8 +356,10 @@ var IntegrationBase = class IntegrationBase {
|
|
|
276
356
|
this.requestExceptionCount = 0;
|
|
277
357
|
}
|
|
278
358
|
async reset() {
|
|
279
|
-
await this.
|
|
280
|
-
|
|
359
|
+
await this.runSessionTransition(async () => {
|
|
360
|
+
await this.disconnectLocked({ silent: true }, true);
|
|
361
|
+
await this.ctx.storage.deleteWorkspace(this.connectedKey);
|
|
362
|
+
});
|
|
281
363
|
}
|
|
282
364
|
/**
|
|
283
365
|
* Drops the in-memory session so the next access re-resolves it from storage. Used when the primary
|
|
@@ -286,24 +368,30 @@ var IntegrationBase = class IntegrationBase {
|
|
|
286
368
|
*/
|
|
287
369
|
switchConnection() {
|
|
288
370
|
this.onStoredTokensReplaced();
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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?.();
|
|
302
384
|
});
|
|
303
|
-
await this.providerOnDisconnect?.();
|
|
304
385
|
}
|
|
305
386
|
skippedNonCloudReported = false;
|
|
306
|
-
|
|
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) {
|
|
307
395
|
const scope = getScopedLogger();
|
|
308
396
|
if (this._session?.cloud === false && state !== "connected") {
|
|
309
397
|
if (this.id !== "github" && !this.skippedNonCloudReported) {
|
|
@@ -316,14 +404,13 @@ var IntegrationBase = class IntegrationBase {
|
|
|
316
404
|
}
|
|
317
405
|
return;
|
|
318
406
|
}
|
|
407
|
+
let failure;
|
|
319
408
|
switch (state) {
|
|
320
409
|
case "connected": {
|
|
321
410
|
const oldSession = this._session;
|
|
322
411
|
let resyncing = false;
|
|
323
412
|
if (forceSync) {
|
|
324
|
-
await (await this.authenticationService.get(this.authProvider.id)).deleteSession(this.authProviderDescriptor);
|
|
325
413
|
this.onStoredTokensReplaced();
|
|
326
|
-
this._session = void 0;
|
|
327
414
|
await this.ctx.storage.deleteWorkspace(this.connectedKey);
|
|
328
415
|
resyncing = true;
|
|
329
416
|
} else {
|
|
@@ -333,17 +420,24 @@ var IntegrationBase = class IntegrationBase {
|
|
|
333
420
|
let newSession;
|
|
334
421
|
let refetchFailed = false;
|
|
335
422
|
try {
|
|
336
|
-
newSession = await this.
|
|
423
|
+
newSession = await this.resolveSessionLocked({
|
|
424
|
+
sync: forceSync,
|
|
425
|
+
refetch: resyncing
|
|
426
|
+
});
|
|
337
427
|
} catch (ex) {
|
|
338
|
-
if (!isCancellationError(ex)) scope?.error(ex);
|
|
339
428
|
refetchFailed = true;
|
|
429
|
+
if (!isCancellationError(ex)) scope?.error(ex);
|
|
430
|
+
failure = toError(ex);
|
|
340
431
|
}
|
|
341
432
|
if (oldSession && newSession && newSession.accessToken !== oldSession.accessToken) this.resetRequestExceptionCount("all");
|
|
342
433
|
if (resyncing && newSession == null && !refetchFailed) await (await this.authenticationService.get(this.authProvider.id)).deleteSession(this.authProviderDescriptor, { preserveConfigured: false });
|
|
343
434
|
break;
|
|
344
435
|
}
|
|
345
|
-
case "disconnected": await this.
|
|
436
|
+
case "disconnected": await this.disconnectLocked({ silent: true }, true);
|
|
346
437
|
}
|
|
438
|
+
this._resyncs++;
|
|
439
|
+
this._lastResyncFailure = failure;
|
|
440
|
+
return failure;
|
|
347
441
|
}
|
|
348
442
|
/**
|
|
349
443
|
* Credentials that passed {@link validateCredential} within the last minute. A probe already in flight is
|
|
@@ -507,24 +601,48 @@ var IntegrationBase = class IntegrationBase {
|
|
|
507
601
|
}
|
|
508
602
|
if (ex instanceof AuthenticationError && this._session?.cloud && !this.hasSessionSyncRequests()) {
|
|
509
603
|
this.requestSessionSyncForUsecase(syncReqUsecase);
|
|
510
|
-
this._session
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
604
|
+
const refused = this._session.accessToken;
|
|
605
|
+
this.expireSessionIfToken(refused);
|
|
606
|
+
this.runSessionTransition(() => {
|
|
607
|
+
this.expireSessionIfToken(refused);
|
|
608
|
+
return Promise.resolve();
|
|
609
|
+
});
|
|
514
610
|
return false;
|
|
515
611
|
}
|
|
516
612
|
if (!(ex instanceof AuthenticationError) && !(ex instanceof RequestClientError)) return false;
|
|
517
613
|
this.trackRequestException(options);
|
|
518
614
|
return true;
|
|
519
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;
|
|
520
626
|
missingExpirityReported = false;
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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) {
|
|
528
646
|
this.ctx.hooks?.session?.onRefreshSkipped?.({
|
|
529
647
|
id: this.id,
|
|
530
648
|
reason: "missing-expiry",
|
|
@@ -541,12 +659,37 @@ var IntegrationBase = class IntegrationBase {
|
|
|
541
659
|
}
|
|
542
660
|
}
|
|
543
661
|
async isConnected() {
|
|
544
|
-
|
|
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);
|
|
545
667
|
}
|
|
546
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) {
|
|
547
690
|
const scope = getScopedLogger();
|
|
548
|
-
const { createIfNeeded,
|
|
549
|
-
if (this._session != null) {
|
|
691
|
+
const { createIfNeeded, source, sync, refetch } = options;
|
|
692
|
+
if (this._session != null && !refetch) {
|
|
550
693
|
if (this.isSessionForIntegrationHost(this._session)) return this._session;
|
|
551
694
|
this._session = null;
|
|
552
695
|
}
|
|
@@ -557,10 +700,10 @@ var IntegrationBase = class IntegrationBase {
|
|
|
557
700
|
try {
|
|
558
701
|
session = await (await this.authenticationService.get(this.authProvider.id)).getSession(this.authProviderDescriptor, sync ? {
|
|
559
702
|
sync,
|
|
703
|
+
refetch,
|
|
560
704
|
source
|
|
561
705
|
} : {
|
|
562
706
|
createIfNeeded,
|
|
563
|
-
forceNewSession,
|
|
564
707
|
source
|
|
565
708
|
});
|
|
566
709
|
if (session?.expiresAt != null && session.expiresAt < /* @__PURE__ */ new Date()) session = null;
|
|
@@ -611,7 +754,7 @@ var IntegrationBase = class IntegrationBase {
|
|
|
611
754
|
async searchMyIssuesResult(resources, cancellation, connectionId) {
|
|
612
755
|
const scope = getScopedLogger();
|
|
613
756
|
const session = await this.resolveReadSession(connectionId, scope);
|
|
614
|
-
if (session == null) return
|
|
757
|
+
if (session == null || isReadSessionFailure(session)) return session && { error: session.error };
|
|
615
758
|
const start = performance.now();
|
|
616
759
|
try {
|
|
617
760
|
const issues = await this.searchProviderMyIssues(session, resources != null ? Array.isArray(resources) ? resources : [resources] : void 0, cancellation);
|
|
@@ -650,7 +793,7 @@ var IntegrationBase = class IntegrationBase {
|
|
|
650
793
|
async searchMyIssuesWithTruncationResult(resources, cancellation, connectionId, options) {
|
|
651
794
|
const scope = getScopedLogger();
|
|
652
795
|
const session = await this.resolveReadSession(connectionId, scope);
|
|
653
|
-
if (session == null) return
|
|
796
|
+
if (session == null || isReadSessionFailure(session)) return session && { error: session.error };
|
|
654
797
|
try {
|
|
655
798
|
const result = await this.searchProviderMyIssuesWithTruncation(session, resources != null ? Array.isArray(resources) ? resources : [resources] : void 0, cancellation, options);
|
|
656
799
|
await this.confirmScopedAuthFailures(session, result?.metadata);
|
|
@@ -666,8 +809,7 @@ var IntegrationBase = class IntegrationBase {
|
|
|
666
809
|
}
|
|
667
810
|
async getLinkedIssueOrPullRequest(resource, link, options) {
|
|
668
811
|
const scope = getScopedLogger();
|
|
669
|
-
if (
|
|
670
|
-
await this.refreshSessionIfExpired(scope);
|
|
812
|
+
if (await this.prepareSessionLookup() != null) return void 0;
|
|
671
813
|
return this.ctx.cache.getIssueOrPullRequest(link.key, options?.type, resource, this, () => ({ value: (async () => {
|
|
672
814
|
try {
|
|
673
815
|
const result = await this.getProviderLinkedIssueOrPullRequest(this._session, resource, link, options?.type);
|
|
@@ -690,7 +832,7 @@ var IntegrationBase = class IntegrationBase {
|
|
|
690
832
|
const { connectionId: requestedConnectionId, expiryOverride } = options ?? {};
|
|
691
833
|
const connectionId = requestedConnectionId || void 0;
|
|
692
834
|
const session = await this.resolveReadSession(connectionId, scope);
|
|
693
|
-
if (session == null) return
|
|
835
|
+
if (session == null || isReadSessionFailure(session)) return session && { error: session.error };
|
|
694
836
|
try {
|
|
695
837
|
return { value: await getCachedIssue({
|
|
696
838
|
cache: this.ctx.cache,
|
|
@@ -724,7 +866,7 @@ var IntegrationBase = class IntegrationBase {
|
|
|
724
866
|
const scope = getScopedLogger();
|
|
725
867
|
const { connectionId: requestedConnectionId, expiryOverride, ...opts } = options ?? {};
|
|
726
868
|
const connectionId = requestedConnectionId || void 0;
|
|
727
|
-
const session = await this.
|
|
869
|
+
const session = await this.resolveReadSessionOrThrow(connectionId, scope);
|
|
728
870
|
if (session == null) return void 0;
|
|
729
871
|
const sessionFingerprint = this.getSessionFingerprint(session);
|
|
730
872
|
return await this.ctx.cache.getCurrentAccount(this, (cacheable) => ({ value: (async () => {
|
|
@@ -765,8 +907,12 @@ var IntegrationBase = class IntegrationBase {
|
|
|
765
907
|
}
|
|
766
908
|
async getPullRequest(resource, id, options) {
|
|
767
909
|
const scope = getScopedLogger();
|
|
768
|
-
|
|
769
|
-
|
|
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
|
+
}
|
|
770
916
|
return await this.ctx.cache.getPullRequest(id, resource, this, (cacheable) => ({ value: (async () => {
|
|
771
917
|
try {
|
|
772
918
|
const result = await this.getProviderPullRequest?.(this._session, resource, id);
|
|
@@ -794,6 +940,6 @@ __decorate([trace()], IntegrationBase.prototype, "getLinkedIssueOrPullRequest",
|
|
|
794
940
|
__decorate([trace()], IntegrationBase.prototype, "getIssueResultCore", null);
|
|
795
941
|
__decorate([trace()], IntegrationBase.prototype, "getPullRequest", null);
|
|
796
942
|
//#endregion
|
|
797
|
-
export { IntegrationBase, issueEtagIncludes, pullRequestEtagIncludes, pullRequestRevision };
|
|
943
|
+
export { IntegrationBase, ReadSessionFailure, isReadSessionFailure, issueEtagIncludes, pullRequestEtagIncludes, pullRequestRevision };
|
|
798
944
|
|
|
799
945
|
//# sourceMappingURL=integration.js.map
|