@gitkraken/core-gitlens 0.3.1 → 0.4.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.
- package/CHANGELOG.md +13 -1
- package/README.md +13 -10
- package/dist/git/context.d.ts +4 -3
- package/dist/git/context.d.ts.map +1 -1
- package/dist/git/context.js.map +1 -1
- package/dist/git/providers/operations.d.ts +1 -0
- package/dist/git/providers/operations.d.ts.map +1 -1
- package/dist/git/providers/worktrees.d.ts +2 -0
- package/dist/git/providers/worktrees.d.ts.map +1 -1
- package/dist/git/utils/conflictResolution.utils.d.ts +32 -0
- package/dist/git/utils/conflictResolution.utils.d.ts.map +1 -1
- package/dist/git/utils/conflictResolution.utils.js +95 -0
- package/dist/git/utils/conflictResolution.utils.js.map +1 -1
- package/dist/git-cli/providers/operations.d.ts +1 -0
- package/dist/git-cli/providers/operations.d.ts.map +1 -1
- package/dist/git-cli/providers/operations.js +22 -1
- package/dist/git-cli/providers/operations.js.map +1 -1
- package/dist/git-cli/providers/worktrees.d.ts +2 -0
- package/dist/git-cli/providers/worktrees.d.ts.map +1 -1
- package/dist/git-cli/providers/worktrees.js +3 -0
- package/dist/git-cli/providers/worktrees.js.map +1 -1
- package/dist/plus/ai/utils/ai.utils.js +5 -5
- package/dist/plus/ai/utils/ai.utils.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts +6 -13
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +5 -10
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/integrations/authentication/cloudIntegrationService.d.ts +11 -0
- package/dist/plus/integrations/authentication/cloudIntegrationService.d.ts.map +1 -0
- package/dist/plus/integrations/authentication/cloudIntegrationService.js +90 -0
- package/dist/plus/integrations/authentication/cloudIntegrationService.js.map +1 -0
- package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts +67 -0
- package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts.map +1 -0
- package/dist/plus/integrations/authentication/configuredIntegrationService.js +295 -0
- package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -0
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts +92 -0
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts.map +1 -0
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +185 -0
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -0
- package/dist/plus/integrations/authentication/integrationAuthenticationService.d.ts +62 -0
- package/dist/plus/integrations/authentication/integrationAuthenticationService.d.ts.map +1 -0
- package/dist/plus/integrations/authentication/integrationAuthenticationService.js +122 -0
- package/dist/plus/integrations/authentication/integrationAuthenticationService.js.map +1 -0
- package/dist/plus/integrations/authentication/manualTokenProvider.d.ts +15 -0
- package/dist/plus/integrations/authentication/manualTokenProvider.d.ts.map +1 -0
- package/dist/plus/integrations/authentication/manualTokenProvider.js +49 -0
- package/dist/plus/integrations/authentication/manualTokenProvider.js.map +1 -0
- package/dist/plus/integrations/authentication/models.d.ts +87 -0
- package/dist/plus/integrations/authentication/models.d.ts.map +1 -0
- package/dist/plus/integrations/authentication/models.js +64 -0
- package/dist/plus/integrations/authentication/models.js.map +1 -0
- package/dist/plus/integrations/constants.d.ts +47 -0
- package/dist/plus/integrations/constants.d.ts.map +1 -0
- package/dist/plus/integrations/constants.js +128 -0
- package/dist/plus/integrations/constants.js.map +1 -0
- package/dist/plus/integrations/context.d.ts +354 -0
- package/dist/plus/integrations/context.d.ts.map +1 -0
- package/dist/plus/integrations/context.js +2 -0
- package/dist/plus/integrations/context.js.map +1 -0
- package/dist/plus/integrations/errors.d.ts +14 -0
- package/dist/plus/integrations/errors.d.ts.map +1 -0
- package/dist/plus/integrations/errors.js +19 -0
- package/dist/plus/integrations/errors.js.map +1 -0
- package/dist/plus/integrations/index.d.ts +32 -0
- package/dist/plus/integrations/index.d.ts.map +1 -0
- package/dist/plus/integrations/index.js +62 -0
- package/dist/plus/integrations/index.js.map +1 -0
- package/dist/plus/integrations/integrationService.d.ts +98 -0
- package/dist/plus/integrations/integrationService.d.ts.map +1 -0
- package/dist/plus/integrations/integrationService.js +627 -0
- package/dist/plus/integrations/integrationService.js.map +1 -0
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +82 -0
- package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -0
- package/dist/plus/integrations/models/gitHostIntegration.js +522 -0
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -0
- package/dist/plus/integrations/models/integration.d.ts +127 -0
- package/dist/plus/integrations/models/integration.d.ts.map +1 -0
- package/dist/plus/integrations/models/integration.js +456 -0
- package/dist/plus/integrations/models/integration.js.map +1 -0
- package/dist/plus/integrations/models/issuesIntegration.d.ts +28 -0
- package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -0
- package/dist/plus/integrations/models/issuesIntegration.js +101 -0
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -0
- package/dist/plus/integrations/providers/apiClients.d.ts +25 -0
- package/dist/plus/integrations/providers/apiClients.d.ts.map +1 -0
- package/dist/plus/integrations/providers/apiClients.js +39 -0
- package/dist/plus/integrations/providers/apiClients.js.map +1 -0
- package/dist/plus/integrations/providers/azure/azure.d.ts +47 -0
- package/dist/plus/integrations/providers/azure/azure.d.ts.map +1 -0
- package/dist/plus/integrations/providers/azure/azure.js +444 -0
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -0
- package/dist/plus/integrations/providers/azure/models.d.ts +286 -0
- package/dist/plus/integrations/providers/azure/models.d.ts.map +1 -0
- package/dist/plus/integrations/providers/azure/models.js +203 -0
- package/dist/plus/integrations/providers/azure/models.js.map +1 -0
- package/dist/plus/integrations/providers/azureDevOps.d.ts +95 -0
- package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -0
- package/dist/plus/integrations/providers/azureDevOps.js +432 -0
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket/bitbucket.d.ts +42 -0
- package/dist/plus/integrations/providers/bitbucket/bitbucket.d.ts.map +1 -0
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js +513 -0
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket/models.d.ts +222 -0
- package/dist/plus/integrations/providers/bitbucket/models.d.ts.map +1 -0
- package/dist/plus/integrations/providers/bitbucket/models.js +158 -0
- package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +120 -0
- package/dist/plus/integrations/providers/bitbucket-server/models.d.ts.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server/models.js +111 -0
- package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +60 -0
- package/dist/plus/integrations/providers/bitbucket-server.d.ts.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server.js +138 -0
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket.d.ts +50 -0
- package/dist/plus/integrations/providers/bitbucket.d.ts.map +1 -0
- package/dist/plus/integrations/providers/bitbucket.js +181 -0
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -0
- package/dist/plus/integrations/providers/github/github.d.ts +5 -0
- package/dist/plus/integrations/providers/github/github.d.ts.map +1 -0
- package/dist/plus/integrations/providers/github/github.js +47 -0
- package/dist/plus/integrations/providers/github/github.js.map +1 -0
- package/dist/plus/integrations/providers/github/github.utils.d.ts +11 -0
- package/dist/plus/integrations/providers/github/github.utils.d.ts.map +1 -0
- package/dist/plus/integrations/providers/github/github.utils.js +6 -0
- package/dist/plus/integrations/providers/github/github.utils.js.map +1 -0
- package/dist/plus/integrations/providers/github/models.d.ts +3 -0
- package/dist/plus/integrations/providers/github/models.d.ts.map +1 -0
- package/dist/plus/integrations/providers/github/models.js +2 -0
- package/dist/plus/integrations/providers/github/models.js.map +1 -0
- package/dist/plus/integrations/providers/github.d.ts +73 -0
- package/dist/plus/integrations/providers/github.d.ts.map +1 -0
- package/dist/plus/integrations/providers/github.js +160 -0
- package/dist/plus/integrations/providers/github.js.map +1 -0
- package/dist/plus/integrations/providers/gitlab/gitlab.d.ts +63 -0
- package/dist/plus/integrations/providers/gitlab/gitlab.d.ts.map +1 -0
- package/dist/plus/integrations/providers/gitlab/gitlab.js +828 -0
- package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -0
- package/dist/plus/integrations/providers/gitlab/gitlab.utils.d.ts +21 -0
- package/dist/plus/integrations/providers/gitlab/gitlab.utils.d.ts.map +1 -0
- package/dist/plus/integrations/providers/gitlab/gitlab.utils.js +45 -0
- package/dist/plus/integrations/providers/gitlab/gitlab.utils.js.map +1 -0
- package/dist/plus/integrations/providers/gitlab/models.d.ts +128 -0
- package/dist/plus/integrations/providers/gitlab/models.d.ts.map +1 -0
- package/dist/plus/integrations/providers/gitlab/models.js +96 -0
- package/dist/plus/integrations/providers/gitlab/models.js.map +1 -0
- package/dist/plus/integrations/providers/gitlab.d.ts +79 -0
- package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -0
- package/dist/plus/integrations/providers/gitlab.js +253 -0
- package/dist/plus/integrations/providers/gitlab.js.map +1 -0
- package/dist/plus/integrations/providers/jira.d.ts +46 -0
- package/dist/plus/integrations/providers/jira.d.ts.map +1 -0
- package/dist/plus/integrations/providers/jira.js +255 -0
- package/dist/plus/integrations/providers/jira.js.map +1 -0
- package/dist/plus/integrations/providers/linear.d.ts +47 -0
- package/dist/plus/integrations/providers/linear.d.ts.map +1 -0
- package/dist/plus/integrations/providers/linear.js +198 -0
- package/dist/plus/integrations/providers/linear.js.map +1 -0
- package/dist/plus/integrations/providers/models.d.ts +433 -0
- package/dist/plus/integrations/providers/models.d.ts.map +1 -0
- package/dist/plus/integrations/providers/models.js +602 -0
- package/dist/plus/integrations/providers/models.js.map +1 -0
- package/dist/plus/integrations/providers/providersApi.d.ts +129 -0
- package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -0
- package/dist/plus/integrations/providers/providersApi.js +602 -0
- package/dist/plus/integrations/providers/providersApi.js.map +1 -0
- package/dist/plus/integrations/providers/utils.d.ts +54 -0
- package/dist/plus/integrations/providers/utils.d.ts.map +1 -0
- package/dist/plus/integrations/providers/utils.js +262 -0
- package/dist/plus/integrations/providers/utils.js.map +1 -0
- package/dist/plus/integrations/telemetry.d.ts +26 -0
- package/dist/plus/integrations/telemetry.d.ts.map +1 -0
- package/dist/plus/integrations/telemetry.js +11 -0
- package/dist/plus/integrations/telemetry.js.map +1 -0
- package/dist/plus/integrations/utils/integration.utils.d.ts +14 -0
- package/dist/plus/integrations/utils/integration.utils.d.ts.map +1 -0
- package/dist/plus/integrations/utils/integration.utils.js +111 -0
- package/dist/plus/integrations/utils/integration.utils.js.map +1 -0
- package/dist/utils/disposable.d.ts +15 -2
- package/dist/utils/disposable.d.ts.map +1 -1
- package/dist/utils/disposable.js.map +1 -1
- package/package.json +43 -2
- package/src/git/context.ts +4 -3
- package/src/git/providers/operations.ts +1 -1
- package/src/git/providers/worktrees.ts +14 -2
- package/src/git/utils/conflictResolution.utils.ts +126 -0
- package/src/git-cli/providers/operations.ts +22 -2
- package/src/git-cli/providers/worktrees.ts +17 -2
- package/src/plus/ai/utils/ai.utils.ts +5 -5
- package/src/plus/git-github/api/github.ts +13 -25
- package/src/plus/integrations/authentication/cloudIntegrationService.ts +120 -0
- package/src/plus/integrations/authentication/configuredIntegrationService.ts +414 -0
- package/src/plus/integrations/authentication/integrationAuthenticationProvider.ts +289 -0
- package/src/plus/integrations/authentication/integrationAuthenticationService.ts +144 -0
- package/src/plus/integrations/authentication/manualTokenProvider.ts +72 -0
- package/src/plus/integrations/authentication/models.ts +168 -0
- package/src/plus/integrations/constants.ts +169 -0
- package/src/plus/integrations/context.ts +383 -0
- package/src/plus/integrations/errors.ts +35 -0
- package/src/plus/integrations/index.ts +119 -0
- package/src/plus/integrations/integrationService.ts +924 -0
- package/src/plus/integrations/models/gitHostIntegration.ts +738 -0
- package/src/plus/integrations/models/integration.ts +657 -0
- package/src/plus/integrations/models/issuesIntegration.ts +124 -0
- package/src/plus/integrations/providers/apiClients.ts +72 -0
- package/src/plus/integrations/providers/azure/azure.ts +691 -0
- package/src/plus/integrations/providers/azure/models.ts +605 -0
- package/src/plus/integrations/providers/azureDevOps.ts +677 -0
- package/src/plus/integrations/providers/bitbucket/bitbucket.ts +774 -0
- package/src/plus/integrations/providers/bitbucket/models.ts +445 -0
- package/src/plus/integrations/providers/bitbucket-server/models.ts +252 -0
- package/src/plus/integrations/providers/bitbucket-server.ts +282 -0
- package/src/plus/integrations/providers/bitbucket.ts +350 -0
- package/src/plus/integrations/providers/github/github.ts +64 -0
- package/src/plus/integrations/providers/github/github.utils.ts +23 -0
- package/src/plus/integrations/providers/github/models.ts +29 -0
- package/src/plus/integrations/providers/github.ts +366 -0
- package/src/plus/integrations/providers/gitlab/gitlab.ts +1210 -0
- package/src/plus/integrations/providers/gitlab/gitlab.utils.ts +71 -0
- package/src/plus/integrations/providers/gitlab/models.ts +257 -0
- package/src/plus/integrations/providers/gitlab.ts +473 -0
- package/src/plus/integrations/providers/jira.ts +359 -0
- package/src/plus/integrations/providers/linear.ts +275 -0
- package/src/plus/integrations/providers/models.ts +1114 -0
- package/src/plus/integrations/providers/providersApi.ts +1126 -0
- package/src/plus/integrations/providers/utils.ts +359 -0
- package/src/plus/integrations/telemetry.ts +29 -0
- package/src/plus/integrations/utils/integration.utils.ts +138 -0
- package/src/utils/disposable.ts +15 -2
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { debounce } from '../../../utils/debounce.js';
|
|
2
|
+
import type { Disposable } from '../../../utils/disposable.js';
|
|
3
|
+
import type { Event } from '../../../utils/event.js';
|
|
4
|
+
import { Emitter } from '../../../utils/event.js';
|
|
5
|
+
import { flatten } from '../../../utils/iterable.js';
|
|
6
|
+
import type {
|
|
7
|
+
IntegrationIds,
|
|
8
|
+
StoredConfiguredIntegrationDescriptor,
|
|
9
|
+
StoredIntegrationConfigurations,
|
|
10
|
+
} from '../constants.js';
|
|
11
|
+
import type { IntegrationServiceContext } from '../context.js';
|
|
12
|
+
import { providersMetadata } from '../providers/models.js';
|
|
13
|
+
import { isGitSelfManagedHostIntegrationId } from '../utils/integration.utils.js';
|
|
14
|
+
import type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthenticationProvider.js';
|
|
15
|
+
import type {
|
|
16
|
+
CloudIntegrationAuthType,
|
|
17
|
+
ConfiguredIntegrationDescriptor,
|
|
18
|
+
ProviderAuthenticationSession,
|
|
19
|
+
} from './models.js';
|
|
20
|
+
|
|
21
|
+
interface StoredSession {
|
|
22
|
+
id: string;
|
|
23
|
+
accessToken: string;
|
|
24
|
+
account?: { label?: string; displayName?: string; id: string };
|
|
25
|
+
scopes: string[];
|
|
26
|
+
cloud?: boolean;
|
|
27
|
+
type: CloudIntegrationAuthType | undefined;
|
|
28
|
+
expiresAt?: string;
|
|
29
|
+
domain?: string;
|
|
30
|
+
protocol?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ConfiguredIntegrationsChangeEvent {
|
|
34
|
+
readonly added: readonly IntegrationIds[];
|
|
35
|
+
readonly removed: readonly IntegrationIds[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class ConfiguredIntegrationService implements Disposable {
|
|
39
|
+
private readonly _onDidChange = new Emitter<ConfiguredIntegrationsChangeEvent>();
|
|
40
|
+
get onDidChange(): Event<ConfiguredIntegrationsChangeEvent> {
|
|
41
|
+
return this._onDidChange.event;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
constructor(private readonly ctx: IntegrationServiceContext) {}
|
|
45
|
+
|
|
46
|
+
dispose(): void {
|
|
47
|
+
this._onDidChange.dispose();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private _configured?: Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>;
|
|
51
|
+
private get configured(): Map<IntegrationIds, ConfiguredIntegrationDescriptor[]> {
|
|
52
|
+
if (this._configured == null) {
|
|
53
|
+
this._configured = new Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>();
|
|
54
|
+
|
|
55
|
+
const storedConfigured = this.ctx.storage.get('integrations:configured');
|
|
56
|
+
for (const [id, configured] of Object.entries(storedConfigured ?? {}) as [
|
|
57
|
+
IntegrationIds,
|
|
58
|
+
StoredConfiguredIntegrationDescriptor[],
|
|
59
|
+
][]) {
|
|
60
|
+
if (configured == null) continue;
|
|
61
|
+
|
|
62
|
+
const descriptors = configured.map(d => ({
|
|
63
|
+
...d,
|
|
64
|
+
expiresAt: d.expiresAt ? new Date(d.expiresAt) : undefined,
|
|
65
|
+
}));
|
|
66
|
+
this._configured.set(id, descriptors);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return this._configured;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Synchronous: reads the in-memory configured map (lazily hydrated from storage). No async work —
|
|
74
|
+
// the old async variant only existed to weave in the built-in VS Code GitHub session, which is gone.
|
|
75
|
+
getConfigured(
|
|
76
|
+
id?: IntegrationIds,
|
|
77
|
+
options?: { cloud?: boolean; domain?: string },
|
|
78
|
+
): ConfiguredIntegrationDescriptor[] {
|
|
79
|
+
const descriptors: ConfiguredIntegrationDescriptor[] = [];
|
|
80
|
+
|
|
81
|
+
const configured =
|
|
82
|
+
id != null
|
|
83
|
+
? this.configured.get(id)
|
|
84
|
+
: [...flatten<ConfiguredIntegrationDescriptor>(this.configured.values())];
|
|
85
|
+
if (!configured?.length) return descriptors;
|
|
86
|
+
|
|
87
|
+
if (options?.domain != null || options?.cloud != null) {
|
|
88
|
+
for (const descriptor of configured) {
|
|
89
|
+
if (
|
|
90
|
+
(options?.domain != null && descriptor.domain !== options.domain) ||
|
|
91
|
+
(options?.cloud === true && !descriptor.cloud) ||
|
|
92
|
+
(options?.cloud === false && descriptor.cloud)
|
|
93
|
+
) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
descriptors.push(descriptor);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
descriptors.push(...configured);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return descriptors;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async storeConfigured(): Promise<void> {
|
|
107
|
+
// We need to convert the map to a record to store
|
|
108
|
+
const configured: StoredIntegrationConfigurations = {} as unknown as StoredIntegrationConfigurations;
|
|
109
|
+
for (const [id, descriptors] of this.configured) {
|
|
110
|
+
configured[id] = descriptors.map(d => ({
|
|
111
|
+
...d,
|
|
112
|
+
expiresAt: d.expiresAt
|
|
113
|
+
? d.expiresAt instanceof Date
|
|
114
|
+
? d.expiresAt.toISOString()
|
|
115
|
+
: d.expiresAt
|
|
116
|
+
: undefined,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await this.ctx.storage.store('integrations:configured', configured);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async addOrUpdateConfigured(descriptor: ConfiguredIntegrationDescriptor): Promise<void> {
|
|
124
|
+
const descriptors = this.configured.get(descriptor.integrationId) ?? [];
|
|
125
|
+
const existing = descriptors.find(
|
|
126
|
+
d =>
|
|
127
|
+
d.domain === descriptor.domain &&
|
|
128
|
+
d.integrationId === descriptor.integrationId &&
|
|
129
|
+
d.cloud === descriptor.cloud,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
let changed: boolean;
|
|
133
|
+
if (existing != null) {
|
|
134
|
+
if (existing.expiresAt === descriptor.expiresAt && existing.scopes === descriptor.scopes) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Only fire the change event if the scopes changes (i.e. ignore any expiresAt changes)
|
|
139
|
+
changed = existing.scopes !== descriptor.scopes;
|
|
140
|
+
|
|
141
|
+
// remove the existing descriptor from the array
|
|
142
|
+
descriptors.splice(descriptors.indexOf(existing), 1);
|
|
143
|
+
} else {
|
|
144
|
+
changed = true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
descriptors.push(descriptor);
|
|
148
|
+
this.configured.set(descriptor.integrationId, descriptors);
|
|
149
|
+
|
|
150
|
+
if (changed) {
|
|
151
|
+
this.fireChange(descriptor.integrationId);
|
|
152
|
+
}
|
|
153
|
+
await this.storeConfigured();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async removeConfigured(
|
|
157
|
+
id: IntegrationIds,
|
|
158
|
+
options: { cloud: boolean | undefined; domain: string | undefined },
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
let changed = false;
|
|
161
|
+
const descriptors = [];
|
|
162
|
+
|
|
163
|
+
for (const d of this.configured.get(id) ?? []) {
|
|
164
|
+
if (
|
|
165
|
+
d.domain === options.domain &&
|
|
166
|
+
(options?.cloud == null ||
|
|
167
|
+
(options?.cloud === true && d.cloud === true) ||
|
|
168
|
+
(options?.cloud === false && d.cloud === false))
|
|
169
|
+
) {
|
|
170
|
+
changed = true;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
descriptors.push(d);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.configured.set(id, descriptors);
|
|
178
|
+
|
|
179
|
+
if (changed) {
|
|
180
|
+
this.fireChange(undefined, id);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await this.storeConfigured();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async storeSession(id: IntegrationIds, session: ProviderAuthenticationSession): Promise<void> {
|
|
187
|
+
await this.writeSecret(id, session);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async getStoredSession(
|
|
191
|
+
id: IntegrationIds,
|
|
192
|
+
descriptor: IntegrationAuthenticationSessionDescriptor,
|
|
193
|
+
): Promise<ProviderAuthenticationSession | undefined> {
|
|
194
|
+
const sessionId = this.getSessionId(descriptor);
|
|
195
|
+
let session = await this.readSecret(id, sessionId, false);
|
|
196
|
+
|
|
197
|
+
let cloudIfMissing = false;
|
|
198
|
+
if (session != null) {
|
|
199
|
+
// Check the `expiresAt` field
|
|
200
|
+
// If it has an expiresAt property and the key is the old type, then it's a cloud session,
|
|
201
|
+
// so delete it from the local key and
|
|
202
|
+
// store with the "cloud" type key, and then use that one.
|
|
203
|
+
// Otherwise it's a local session under the local key, so just return it.
|
|
204
|
+
if (session.expiresAt != null) {
|
|
205
|
+
cloudIfMissing = true;
|
|
206
|
+
await Promise.allSettled([this.deleteSecrets(id, session.id), this.writeSecret(id, session)]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// If no local session we try to restore a session with the cloud key
|
|
211
|
+
if (session == null) {
|
|
212
|
+
cloudIfMissing = true;
|
|
213
|
+
session = await this.readSecret(id, sessionId, true);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return convertStoredSessionToSession(session, descriptor, cloudIfMissing);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async deleteStoredSessions(
|
|
220
|
+
id: IntegrationIds,
|
|
221
|
+
descriptor: IntegrationAuthenticationSessionDescriptor,
|
|
222
|
+
cloud?: boolean,
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
await this.deleteSecrets(id, this.getSessionId(descriptor), cloud);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async deleteAllStoredSessions(id: IntegrationIds, cloud?: boolean): Promise<void> {
|
|
228
|
+
await this.deleteAllSecrets(id, cloud);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Reclaims all stored configuration + secrets for the given (typically retired) integration ids.
|
|
233
|
+
* Used to clean up state left behind when an id is removed from the model — e.g. the local
|
|
234
|
+
* self-managed `github-enterprise`/`gitlab-self-hosted` ids dropped when integrations went cloud-only.
|
|
235
|
+
* Domains are read from the stored config (secrets are keyed by domain); ids with no stored config
|
|
236
|
+
* are a no-op.
|
|
237
|
+
*/
|
|
238
|
+
async purgeStoredConfiguration(ids: readonly string[]): Promise<void> {
|
|
239
|
+
const stored =
|
|
240
|
+
this.ctx.storage.get<Record<string, StoredConfiguredIntegrationDescriptor[] | undefined>>(
|
|
241
|
+
'integrations:configured',
|
|
242
|
+
);
|
|
243
|
+
if (stored == null) return;
|
|
244
|
+
|
|
245
|
+
let changed = false;
|
|
246
|
+
for (const id of ids) {
|
|
247
|
+
if (!(id in stored)) continue;
|
|
248
|
+
|
|
249
|
+
for (const descriptor of stored[id] ?? []) {
|
|
250
|
+
const sessionId = descriptor.domain ?? '';
|
|
251
|
+
await this.ctx.storage.deleteSecret(this.getLocalSecretKey(id as IntegrationIds, sessionId));
|
|
252
|
+
await this.ctx.storage.deleteSecret(this.getCloudSecretKey(id as IntegrationIds, sessionId));
|
|
253
|
+
if (descriptor.domain) {
|
|
254
|
+
await this.ctx.storage.deleteWorkspace(`connected:${id}:${descriptor.domain}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
changed = true;
|
|
258
|
+
}
|
|
259
|
+
if (!changed) return;
|
|
260
|
+
|
|
261
|
+
const remaining = Object.fromEntries(Object.entries(stored).filter(([key]) => !ids.includes(key)));
|
|
262
|
+
await this.ctx.storage.store('integrations:configured', remaining as StoredIntegrationConfigurations);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async deleteSecrets(id: IntegrationIds, sessionId: string, cloud?: boolean): Promise<void> {
|
|
266
|
+
if (cloud == null || cloud === false) {
|
|
267
|
+
await this.ctx.storage.deleteSecret(this.getLocalSecretKey(id, sessionId));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (cloud == null || cloud === true) {
|
|
271
|
+
await this.ctx.storage.deleteSecret(this.getCloudSecretKey(id, sessionId));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
await this.removeConfigured(id, {
|
|
275
|
+
cloud: cloud,
|
|
276
|
+
domain: isGitSelfManagedHostIntegrationId(id) ? sessionId : undefined,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async deleteAllSecrets(id: IntegrationIds, cloud?: boolean): Promise<void> {
|
|
281
|
+
if (isGitSelfManagedHostIntegrationId(id)) {
|
|
282
|
+
// Hack because session IDs are tied to domain. Update this when session ids are different
|
|
283
|
+
const configuredDomains = this.configured.get(id)?.map(c => c.domain);
|
|
284
|
+
if (configuredDomains != null) {
|
|
285
|
+
for (const domain of configuredDomains) {
|
|
286
|
+
await this.deleteSecrets(id, domain!, cloud);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await this.deleteSecrets(id, providersMetadata[id].domain, cloud);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async writeSecret(id: IntegrationIds, session: ProviderAuthenticationSession | StoredSession): Promise<void> {
|
|
297
|
+
await this.ctx.storage.storeSecret(
|
|
298
|
+
this.getSecretKey(id, session.id, session.cloud ?? false),
|
|
299
|
+
JSON.stringify(session),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
await this.addOrUpdateConfigured({
|
|
303
|
+
integrationId: id,
|
|
304
|
+
domain: isGitSelfManagedHostIntegrationId(id) ? session.domain : undefined,
|
|
305
|
+
expiresAt: session.expiresAt,
|
|
306
|
+
scopes: session.scopes.join(','),
|
|
307
|
+
cloud: session.cloud ?? false,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async readSecret(
|
|
312
|
+
id: IntegrationIds,
|
|
313
|
+
sessionId: string,
|
|
314
|
+
cloud: boolean = false,
|
|
315
|
+
): Promise<StoredSession | undefined> {
|
|
316
|
+
let storedSession: StoredSession | undefined;
|
|
317
|
+
try {
|
|
318
|
+
const sessionJSON = await this.ctx.storage.getSecret(this.getSecretKey(id, sessionId, cloud));
|
|
319
|
+
if (sessionJSON) {
|
|
320
|
+
storedSession = JSON.parse(sessionJSON);
|
|
321
|
+
if (storedSession != null) {
|
|
322
|
+
const configured = this.configured.get(id);
|
|
323
|
+
const domain = isGitSelfManagedHostIntegrationId(id) ? storedSession.id : undefined;
|
|
324
|
+
if (
|
|
325
|
+
configured == null ||
|
|
326
|
+
configured.length === 0 ||
|
|
327
|
+
!configured.some(c => c.domain === domain && c.integrationId === id)
|
|
328
|
+
) {
|
|
329
|
+
await this.addOrUpdateConfigured({
|
|
330
|
+
integrationId: id,
|
|
331
|
+
domain: domain,
|
|
332
|
+
expiresAt: storedSession.expiresAt,
|
|
333
|
+
scopes: storedSession.scopes.join(','),
|
|
334
|
+
cloud: storedSession.cloud ?? false,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} catch (_ex) {
|
|
340
|
+
try {
|
|
341
|
+
await this.deleteSecrets(id, sessionId, cloud);
|
|
342
|
+
} catch {}
|
|
343
|
+
}
|
|
344
|
+
return storedSession;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private getSecretKey(
|
|
348
|
+
id: IntegrationIds,
|
|
349
|
+
sessionId: string,
|
|
350
|
+
cloud: boolean = false,
|
|
351
|
+
): `integration.auth:${IntegrationIds}|${string}` | `integration.auth.cloud:${IntegrationIds}|${string}` {
|
|
352
|
+
return cloud ? this.getCloudSecretKey(id, sessionId) : this.getLocalSecretKey(id, sessionId);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private getLocalSecretKey(id: IntegrationIds, sessionId: string): `integration.auth:${IntegrationIds}|${string}` {
|
|
356
|
+
return `integration.auth:${id}|${sessionId}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private getCloudSecretKey(
|
|
360
|
+
id: IntegrationIds,
|
|
361
|
+
sessionId: string,
|
|
362
|
+
): `integration.auth.cloud:${IntegrationIds}|${string}` {
|
|
363
|
+
return `integration.auth.cloud:${id}|${sessionId}`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
getSessionId(descriptor: IntegrationAuthenticationSessionDescriptor): string {
|
|
367
|
+
return descriptor.domain;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private _addedIds = new Set<IntegrationIds>();
|
|
371
|
+
private _removedIds = new Set<IntegrationIds>();
|
|
372
|
+
private _fireChangeDebounced?: () => void;
|
|
373
|
+
private fireChange(added?: IntegrationIds, removed?: IntegrationIds) {
|
|
374
|
+
this._fireChangeDebounced ??= debounce(() => {
|
|
375
|
+
const added = [...this._addedIds];
|
|
376
|
+
this._addedIds.clear();
|
|
377
|
+
const removed = [...this._removedIds];
|
|
378
|
+
this._removedIds.clear();
|
|
379
|
+
|
|
380
|
+
this._onDidChange.fire({ added: added, removed: removed });
|
|
381
|
+
}, 250);
|
|
382
|
+
|
|
383
|
+
if (added != null) {
|
|
384
|
+
this._addedIds.add(added);
|
|
385
|
+
}
|
|
386
|
+
if (removed != null) {
|
|
387
|
+
this._removedIds.add(removed);
|
|
388
|
+
}
|
|
389
|
+
this._fireChangeDebounced();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function convertStoredSessionToSession(
|
|
394
|
+
storedSession: StoredSession | undefined,
|
|
395
|
+
descriptor: IntegrationAuthenticationSessionDescriptor,
|
|
396
|
+
cloudIfMissing: boolean,
|
|
397
|
+
): ProviderAuthenticationSession | undefined {
|
|
398
|
+
if (storedSession == null) return undefined;
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
id: storedSession.id,
|
|
402
|
+
accessToken: storedSession.accessToken,
|
|
403
|
+
account: {
|
|
404
|
+
id: storedSession.account?.id ?? '',
|
|
405
|
+
label: storedSession.account?.label ?? '',
|
|
406
|
+
},
|
|
407
|
+
scopes: storedSession.scopes,
|
|
408
|
+
cloud: storedSession.cloud ?? cloudIfMissing,
|
|
409
|
+
expiresAt: storedSession.expiresAt ? new Date(storedSession.expiresAt) : undefined,
|
|
410
|
+
domain: storedSession.domain ?? descriptor.domain,
|
|
411
|
+
protocol: storedSession.protocol,
|
|
412
|
+
type: storedSession.type,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { trace } from '../../../utils/decorators/log.js';
|
|
2
|
+
import type { Disposable } from '../../../utils/disposable.js';
|
|
3
|
+
import type { Event } from '../../../utils/event.js';
|
|
4
|
+
import { Emitter } from '../../../utils/event.js';
|
|
5
|
+
import type { IntegrationIds } from '../constants.js';
|
|
6
|
+
import { GitCloudHostIntegrationId } from '../constants.js';
|
|
7
|
+
import type { Sources } from '../telemetry.js';
|
|
8
|
+
import {
|
|
9
|
+
isCloudGitSelfManagedHostIntegrationId,
|
|
10
|
+
isGitSelfManagedHostIntegrationId,
|
|
11
|
+
} from '../utils/integration.utils.js';
|
|
12
|
+
import type { ConfiguredIntegrationService } from './configuredIntegrationService.js';
|
|
13
|
+
import type { IntegrationAuthenticationService } from './integrationAuthenticationService.js';
|
|
14
|
+
import type { ProviderAuthenticationSession } from './models.js';
|
|
15
|
+
import { isSupportedCloudIntegrationId } from './models.js';
|
|
16
|
+
|
|
17
|
+
const maxSmallIntegerV8 = 2 ** 30 - 1; // Max number that can be stored in V8's smis (small integers)
|
|
18
|
+
|
|
19
|
+
export interface IntegrationAuthenticationProviderDescriptor {
|
|
20
|
+
id: IntegrationIds;
|
|
21
|
+
scopes: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface IntegrationAuthenticationSessionDescriptor {
|
|
25
|
+
domain: string;
|
|
26
|
+
scopes: string[];
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface IntegrationAuthenticationProvider extends Disposable {
|
|
31
|
+
deleteSession(descriptor: IntegrationAuthenticationSessionDescriptor): Promise<void>;
|
|
32
|
+
deleteAllSessions(): Promise<void>;
|
|
33
|
+
getSession(
|
|
34
|
+
descriptor: IntegrationAuthenticationSessionDescriptor,
|
|
35
|
+
options?:
|
|
36
|
+
| { createIfNeeded?: boolean; forceNewSession?: boolean; sync?: never; source?: Sources }
|
|
37
|
+
| { createIfNeeded?: never; forceNewSession?: never; sync: boolean; source?: Sources },
|
|
38
|
+
): Promise<ProviderAuthenticationSession | undefined>;
|
|
39
|
+
get onDidChange(): Event<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
abstract class IntegrationAuthenticationProviderBase<
|
|
43
|
+
ID extends IntegrationIds = IntegrationIds,
|
|
44
|
+
> implements IntegrationAuthenticationProvider {
|
|
45
|
+
protected readonly disposables: Disposable[] = [];
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
protected readonly authenticationService: IntegrationAuthenticationService,
|
|
49
|
+
protected readonly configuredIntegrationService: ConfiguredIntegrationService,
|
|
50
|
+
) {}
|
|
51
|
+
|
|
52
|
+
dispose(): void {
|
|
53
|
+
this.disposables.forEach(d => d.dispose());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private readonly _onDidChange = new Emitter<void>();
|
|
57
|
+
get onDidChange(): Event<void> {
|
|
58
|
+
return this._onDidChange.event;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
protected abstract get authProviderId(): ID;
|
|
62
|
+
|
|
63
|
+
@trace()
|
|
64
|
+
async deleteSession(descriptor: IntegrationAuthenticationSessionDescriptor): Promise<void> {
|
|
65
|
+
const configured = this.configuredIntegrationService.getConfigured(this.authProviderId, {
|
|
66
|
+
cloud: true,
|
|
67
|
+
domain: isGitSelfManagedHostIntegrationId(this.authProviderId) ? descriptor?.domain : undefined,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await this.configuredIntegrationService.deleteStoredSessions(this.authProviderId, descriptor, undefined);
|
|
71
|
+
|
|
72
|
+
if (configured?.length) {
|
|
73
|
+
this.fireChange();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@trace()
|
|
78
|
+
async deleteAllSessions(): Promise<void> {
|
|
79
|
+
const configured = this.configuredIntegrationService.getConfigured(this.authProviderId, {
|
|
80
|
+
cloud: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await this.configuredIntegrationService.deleteAllStoredSessions(this.authProviderId, undefined);
|
|
84
|
+
|
|
85
|
+
if (configured?.length) {
|
|
86
|
+
this.fireChange();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@trace()
|
|
91
|
+
async getSession(
|
|
92
|
+
descriptor: IntegrationAuthenticationSessionDescriptor,
|
|
93
|
+
options?:
|
|
94
|
+
| { createIfNeeded?: boolean; forceNewSession?: boolean; sync?: never; source?: Sources }
|
|
95
|
+
| { createIfNeeded?: never; forceNewSession?: never; sync: boolean; source?: Sources },
|
|
96
|
+
): Promise<ProviderAuthenticationSession | undefined> {
|
|
97
|
+
let session;
|
|
98
|
+
let previousToken;
|
|
99
|
+
if (options?.forceNewSession) {
|
|
100
|
+
await this.configuredIntegrationService.deleteStoredSessions(this.authProviderId, descriptor, undefined);
|
|
101
|
+
} else {
|
|
102
|
+
session = await this.configuredIntegrationService.getStoredSession(this.authProviderId, descriptor);
|
|
103
|
+
previousToken = session?.accessToken;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const isExpiredSession = session?.expiresAt != null && new Date(session.expiresAt).getTime() < Date.now();
|
|
107
|
+
if (session == null || isExpiredSession) {
|
|
108
|
+
session = await this.getNewSession(descriptor, {
|
|
109
|
+
...options,
|
|
110
|
+
refreshIfExpired: isExpiredSession,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (session != null) {
|
|
114
|
+
await this.configuredIntegrationService.storeSession(this.authProviderId, session);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (previousToken !== session?.accessToken) {
|
|
119
|
+
this.fireChange();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return session;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
protected abstract getNewSession(
|
|
126
|
+
descriptor: IntegrationAuthenticationSessionDescriptor,
|
|
127
|
+
options?:
|
|
128
|
+
| {
|
|
129
|
+
createIfNeeded?: boolean;
|
|
130
|
+
forceNewSession?: boolean;
|
|
131
|
+
sync?: never;
|
|
132
|
+
refreshIfExpired?: boolean;
|
|
133
|
+
source?: Sources;
|
|
134
|
+
}
|
|
135
|
+
| {
|
|
136
|
+
createIfNeeded?: never;
|
|
137
|
+
forceNewSession?: never;
|
|
138
|
+
sync: boolean;
|
|
139
|
+
refreshIfExpired?: boolean;
|
|
140
|
+
source?: Sources;
|
|
141
|
+
},
|
|
142
|
+
): Promise<ProviderAuthenticationSession | undefined>;
|
|
143
|
+
|
|
144
|
+
protected fireChange(): void {
|
|
145
|
+
queueMicrotask(() => this._onDidChange.fire());
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export class CloudIntegrationAuthenticationProvider<
|
|
150
|
+
ID extends IntegrationIds = IntegrationIds,
|
|
151
|
+
> extends IntegrationAuthenticationProviderBase<ID> {
|
|
152
|
+
constructor(
|
|
153
|
+
authenticationService: IntegrationAuthenticationService,
|
|
154
|
+
configuredIntegrationService: ConfiguredIntegrationService,
|
|
155
|
+
private readonly _authProviderId: ID,
|
|
156
|
+
) {
|
|
157
|
+
super(authenticationService, configuredIntegrationService);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
protected get authProviderId(): ID {
|
|
161
|
+
return this._authProviderId;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
protected override async getNewSession(
|
|
165
|
+
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
|
+
},
|
|
181
|
+
): Promise<ProviderAuthenticationSession | undefined> {
|
|
182
|
+
if (options?.forceNewSession) {
|
|
183
|
+
if ((await this.disconnectCloudSession()) === 'failure') {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
void this.connectCloudSession(false, options?.source);
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// TODO: This is a stopgap to make sure we're not hammering the api on automatic calls to get the session.
|
|
192
|
+
// Ultimately we want to timestamp calls to syncCloudIntegrations and use that to determine whether we should
|
|
193
|
+
// make the call or not.
|
|
194
|
+
let session =
|
|
195
|
+
options?.refreshIfExpired || options?.createIfNeeded || options?.forceNewSession || options?.sync
|
|
196
|
+
? await this.getCloudSession(descriptor)
|
|
197
|
+
: undefined;
|
|
198
|
+
|
|
199
|
+
const shouldCreateSession = options?.createIfNeeded && session == null;
|
|
200
|
+
if (shouldCreateSession) {
|
|
201
|
+
const connected = await this.connectCloudSession(true, options?.source);
|
|
202
|
+
if (!connected) return undefined;
|
|
203
|
+
|
|
204
|
+
// This should get us the session we just created with connectCloudSession, because a syncCloudIntegrations run from
|
|
205
|
+
// integration service should have resulted in it being created and stored by this provider
|
|
206
|
+
session = await this.getSession(descriptor, { source: options?.source });
|
|
207
|
+
}
|
|
208
|
+
return session;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private connectCloudSession(skipIfConnected: boolean, source: Sources | undefined): Promise<boolean> {
|
|
212
|
+
// Capture in a const so the type guard narrows it to `SupportedCloudIntegrationIds` (a getter
|
|
213
|
+
// access wouldn't narrow), letting us call the service directly without a cast.
|
|
214
|
+
const id = this.authProviderId;
|
|
215
|
+
if (isSupportedCloudIntegrationId(id)) {
|
|
216
|
+
return this.authenticationService.connectCloudIntegrations(
|
|
217
|
+
{ integrationIds: [id], skipIfConnected: skipIfConnected, skipPreSync: true },
|
|
218
|
+
{
|
|
219
|
+
source: source ?? 'integrations',
|
|
220
|
+
detail: {
|
|
221
|
+
action: 'connect',
|
|
222
|
+
integration: id,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return Promise.resolve(false);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private async getCloudSession(
|
|
232
|
+
descriptor: IntegrationAuthenticationSessionDescriptor,
|
|
233
|
+
): Promise<ProviderAuthenticationSession | undefined> {
|
|
234
|
+
const loggedIn = (await this.authenticationService.ctx.account.getAccount()) != null;
|
|
235
|
+
if (!loggedIn) return undefined;
|
|
236
|
+
|
|
237
|
+
const cloudIntegrations = this.authenticationService.cloudIntegrations;
|
|
238
|
+
let session = await cloudIntegrations.getConnectionSession(this.authProviderId);
|
|
239
|
+
|
|
240
|
+
// Make an exception for GitHub and Cloud Self-Hosted integrations because they always return 0
|
|
241
|
+
if (
|
|
242
|
+
session?.expiresIn === 0 &&
|
|
243
|
+
(this.authProviderId === GitCloudHostIntegrationId.GitHub ||
|
|
244
|
+
isCloudGitSelfManagedHostIntegrationId(this.authProviderId))
|
|
245
|
+
) {
|
|
246
|
+
// It never expires so don't refresh it frequently:
|
|
247
|
+
session.expiresIn = maxSmallIntegerV8; // maximum expiration length
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (session != null && session.expiresIn < 60) {
|
|
251
|
+
session = await cloudIntegrations.getConnectionSession(this.authProviderId, session.accessToken);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!session) return undefined;
|
|
255
|
+
|
|
256
|
+
let sessionProtocol;
|
|
257
|
+
try {
|
|
258
|
+
sessionProtocol = new URL(session.domain).protocol;
|
|
259
|
+
} catch {
|
|
260
|
+
// If the domain is invalid, we can't use it to create a session
|
|
261
|
+
sessionProtocol = undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// TODO: Once we care about domains, we should try to match the domain here against ours, and if it fails, return undefined
|
|
265
|
+
return {
|
|
266
|
+
id: this.configuredIntegrationService.getSessionId(descriptor),
|
|
267
|
+
accessToken: session.accessToken,
|
|
268
|
+
scopes: descriptor.scopes,
|
|
269
|
+
account: {
|
|
270
|
+
id: '',
|
|
271
|
+
label: '',
|
|
272
|
+
},
|
|
273
|
+
cloud: true,
|
|
274
|
+
type: session.type,
|
|
275
|
+
expiresAt: new Date(session.expiresIn * 1000 + Date.now()),
|
|
276
|
+
// Note: do not use the session's domain, because the format is different than in our model
|
|
277
|
+
domain: descriptor.domain,
|
|
278
|
+
protocol: sessionProtocol ?? undefined,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private async disconnectCloudSession(): Promise<'skip' | 'success' | 'failure'> {
|
|
283
|
+
const loggedIn = (await this.authenticationService.ctx.account.getAccount()) != null;
|
|
284
|
+
if (!loggedIn) return 'skip';
|
|
285
|
+
|
|
286
|
+
const cloudIntegrations = this.authenticationService.cloudIntegrations;
|
|
287
|
+
return (await cloudIntegrations.disconnect(this.authProviderId)) ? 'success' : 'failure';
|
|
288
|
+
}
|
|
289
|
+
}
|