@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.
Files changed (230) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/README.md +13 -10
  3. package/dist/git/context.d.ts +4 -3
  4. package/dist/git/context.d.ts.map +1 -1
  5. package/dist/git/context.js.map +1 -1
  6. package/dist/git/providers/operations.d.ts +1 -0
  7. package/dist/git/providers/operations.d.ts.map +1 -1
  8. package/dist/git/providers/worktrees.d.ts +2 -0
  9. package/dist/git/providers/worktrees.d.ts.map +1 -1
  10. package/dist/git/utils/conflictResolution.utils.d.ts +32 -0
  11. package/dist/git/utils/conflictResolution.utils.d.ts.map +1 -1
  12. package/dist/git/utils/conflictResolution.utils.js +95 -0
  13. package/dist/git/utils/conflictResolution.utils.js.map +1 -1
  14. package/dist/git-cli/providers/operations.d.ts +1 -0
  15. package/dist/git-cli/providers/operations.d.ts.map +1 -1
  16. package/dist/git-cli/providers/operations.js +22 -1
  17. package/dist/git-cli/providers/operations.js.map +1 -1
  18. package/dist/git-cli/providers/worktrees.d.ts +2 -0
  19. package/dist/git-cli/providers/worktrees.d.ts.map +1 -1
  20. package/dist/git-cli/providers/worktrees.js +3 -0
  21. package/dist/git-cli/providers/worktrees.js.map +1 -1
  22. package/dist/plus/ai/utils/ai.utils.js +5 -5
  23. package/dist/plus/ai/utils/ai.utils.js.map +1 -1
  24. package/dist/plus/git-github/api/github.d.ts +6 -13
  25. package/dist/plus/git-github/api/github.d.ts.map +1 -1
  26. package/dist/plus/git-github/api/github.js +5 -10
  27. package/dist/plus/git-github/api/github.js.map +1 -1
  28. package/dist/plus/integrations/authentication/cloudIntegrationService.d.ts +11 -0
  29. package/dist/plus/integrations/authentication/cloudIntegrationService.d.ts.map +1 -0
  30. package/dist/plus/integrations/authentication/cloudIntegrationService.js +90 -0
  31. package/dist/plus/integrations/authentication/cloudIntegrationService.js.map +1 -0
  32. package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts +67 -0
  33. package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts.map +1 -0
  34. package/dist/plus/integrations/authentication/configuredIntegrationService.js +295 -0
  35. package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -0
  36. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts +92 -0
  37. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts.map +1 -0
  38. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +185 -0
  39. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -0
  40. package/dist/plus/integrations/authentication/integrationAuthenticationService.d.ts +62 -0
  41. package/dist/plus/integrations/authentication/integrationAuthenticationService.d.ts.map +1 -0
  42. package/dist/plus/integrations/authentication/integrationAuthenticationService.js +122 -0
  43. package/dist/plus/integrations/authentication/integrationAuthenticationService.js.map +1 -0
  44. package/dist/plus/integrations/authentication/manualTokenProvider.d.ts +15 -0
  45. package/dist/plus/integrations/authentication/manualTokenProvider.d.ts.map +1 -0
  46. package/dist/plus/integrations/authentication/manualTokenProvider.js +49 -0
  47. package/dist/plus/integrations/authentication/manualTokenProvider.js.map +1 -0
  48. package/dist/plus/integrations/authentication/models.d.ts +87 -0
  49. package/dist/plus/integrations/authentication/models.d.ts.map +1 -0
  50. package/dist/plus/integrations/authentication/models.js +64 -0
  51. package/dist/plus/integrations/authentication/models.js.map +1 -0
  52. package/dist/plus/integrations/constants.d.ts +47 -0
  53. package/dist/plus/integrations/constants.d.ts.map +1 -0
  54. package/dist/plus/integrations/constants.js +128 -0
  55. package/dist/plus/integrations/constants.js.map +1 -0
  56. package/dist/plus/integrations/context.d.ts +354 -0
  57. package/dist/plus/integrations/context.d.ts.map +1 -0
  58. package/dist/plus/integrations/context.js +2 -0
  59. package/dist/plus/integrations/context.js.map +1 -0
  60. package/dist/plus/integrations/errors.d.ts +14 -0
  61. package/dist/plus/integrations/errors.d.ts.map +1 -0
  62. package/dist/plus/integrations/errors.js +19 -0
  63. package/dist/plus/integrations/errors.js.map +1 -0
  64. package/dist/plus/integrations/index.d.ts +32 -0
  65. package/dist/plus/integrations/index.d.ts.map +1 -0
  66. package/dist/plus/integrations/index.js +62 -0
  67. package/dist/plus/integrations/index.js.map +1 -0
  68. package/dist/plus/integrations/integrationService.d.ts +98 -0
  69. package/dist/plus/integrations/integrationService.d.ts.map +1 -0
  70. package/dist/plus/integrations/integrationService.js +627 -0
  71. package/dist/plus/integrations/integrationService.js.map +1 -0
  72. package/dist/plus/integrations/models/gitHostIntegration.d.ts +82 -0
  73. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -0
  74. package/dist/plus/integrations/models/gitHostIntegration.js +522 -0
  75. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -0
  76. package/dist/plus/integrations/models/integration.d.ts +127 -0
  77. package/dist/plus/integrations/models/integration.d.ts.map +1 -0
  78. package/dist/plus/integrations/models/integration.js +456 -0
  79. package/dist/plus/integrations/models/integration.js.map +1 -0
  80. package/dist/plus/integrations/models/issuesIntegration.d.ts +28 -0
  81. package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -0
  82. package/dist/plus/integrations/models/issuesIntegration.js +101 -0
  83. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -0
  84. package/dist/plus/integrations/providers/apiClients.d.ts +25 -0
  85. package/dist/plus/integrations/providers/apiClients.d.ts.map +1 -0
  86. package/dist/plus/integrations/providers/apiClients.js +39 -0
  87. package/dist/plus/integrations/providers/apiClients.js.map +1 -0
  88. package/dist/plus/integrations/providers/azure/azure.d.ts +47 -0
  89. package/dist/plus/integrations/providers/azure/azure.d.ts.map +1 -0
  90. package/dist/plus/integrations/providers/azure/azure.js +444 -0
  91. package/dist/plus/integrations/providers/azure/azure.js.map +1 -0
  92. package/dist/plus/integrations/providers/azure/models.d.ts +286 -0
  93. package/dist/plus/integrations/providers/azure/models.d.ts.map +1 -0
  94. package/dist/plus/integrations/providers/azure/models.js +203 -0
  95. package/dist/plus/integrations/providers/azure/models.js.map +1 -0
  96. package/dist/plus/integrations/providers/azureDevOps.d.ts +95 -0
  97. package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -0
  98. package/dist/plus/integrations/providers/azureDevOps.js +432 -0
  99. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -0
  100. package/dist/plus/integrations/providers/bitbucket/bitbucket.d.ts +42 -0
  101. package/dist/plus/integrations/providers/bitbucket/bitbucket.d.ts.map +1 -0
  102. package/dist/plus/integrations/providers/bitbucket/bitbucket.js +513 -0
  103. package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -0
  104. package/dist/plus/integrations/providers/bitbucket/models.d.ts +222 -0
  105. package/dist/plus/integrations/providers/bitbucket/models.d.ts.map +1 -0
  106. package/dist/plus/integrations/providers/bitbucket/models.js +158 -0
  107. package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -0
  108. package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +120 -0
  109. package/dist/plus/integrations/providers/bitbucket-server/models.d.ts.map +1 -0
  110. package/dist/plus/integrations/providers/bitbucket-server/models.js +111 -0
  111. package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -0
  112. package/dist/plus/integrations/providers/bitbucket-server.d.ts +60 -0
  113. package/dist/plus/integrations/providers/bitbucket-server.d.ts.map +1 -0
  114. package/dist/plus/integrations/providers/bitbucket-server.js +138 -0
  115. package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -0
  116. package/dist/plus/integrations/providers/bitbucket.d.ts +50 -0
  117. package/dist/plus/integrations/providers/bitbucket.d.ts.map +1 -0
  118. package/dist/plus/integrations/providers/bitbucket.js +181 -0
  119. package/dist/plus/integrations/providers/bitbucket.js.map +1 -0
  120. package/dist/plus/integrations/providers/github/github.d.ts +5 -0
  121. package/dist/plus/integrations/providers/github/github.d.ts.map +1 -0
  122. package/dist/plus/integrations/providers/github/github.js +47 -0
  123. package/dist/plus/integrations/providers/github/github.js.map +1 -0
  124. package/dist/plus/integrations/providers/github/github.utils.d.ts +11 -0
  125. package/dist/plus/integrations/providers/github/github.utils.d.ts.map +1 -0
  126. package/dist/plus/integrations/providers/github/github.utils.js +6 -0
  127. package/dist/plus/integrations/providers/github/github.utils.js.map +1 -0
  128. package/dist/plus/integrations/providers/github/models.d.ts +3 -0
  129. package/dist/plus/integrations/providers/github/models.d.ts.map +1 -0
  130. package/dist/plus/integrations/providers/github/models.js +2 -0
  131. package/dist/plus/integrations/providers/github/models.js.map +1 -0
  132. package/dist/plus/integrations/providers/github.d.ts +73 -0
  133. package/dist/plus/integrations/providers/github.d.ts.map +1 -0
  134. package/dist/plus/integrations/providers/github.js +160 -0
  135. package/dist/plus/integrations/providers/github.js.map +1 -0
  136. package/dist/plus/integrations/providers/gitlab/gitlab.d.ts +63 -0
  137. package/dist/plus/integrations/providers/gitlab/gitlab.d.ts.map +1 -0
  138. package/dist/plus/integrations/providers/gitlab/gitlab.js +828 -0
  139. package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -0
  140. package/dist/plus/integrations/providers/gitlab/gitlab.utils.d.ts +21 -0
  141. package/dist/plus/integrations/providers/gitlab/gitlab.utils.d.ts.map +1 -0
  142. package/dist/plus/integrations/providers/gitlab/gitlab.utils.js +45 -0
  143. package/dist/plus/integrations/providers/gitlab/gitlab.utils.js.map +1 -0
  144. package/dist/plus/integrations/providers/gitlab/models.d.ts +128 -0
  145. package/dist/plus/integrations/providers/gitlab/models.d.ts.map +1 -0
  146. package/dist/plus/integrations/providers/gitlab/models.js +96 -0
  147. package/dist/plus/integrations/providers/gitlab/models.js.map +1 -0
  148. package/dist/plus/integrations/providers/gitlab.d.ts +79 -0
  149. package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -0
  150. package/dist/plus/integrations/providers/gitlab.js +253 -0
  151. package/dist/plus/integrations/providers/gitlab.js.map +1 -0
  152. package/dist/plus/integrations/providers/jira.d.ts +46 -0
  153. package/dist/plus/integrations/providers/jira.d.ts.map +1 -0
  154. package/dist/plus/integrations/providers/jira.js +255 -0
  155. package/dist/plus/integrations/providers/jira.js.map +1 -0
  156. package/dist/plus/integrations/providers/linear.d.ts +47 -0
  157. package/dist/plus/integrations/providers/linear.d.ts.map +1 -0
  158. package/dist/plus/integrations/providers/linear.js +198 -0
  159. package/dist/plus/integrations/providers/linear.js.map +1 -0
  160. package/dist/plus/integrations/providers/models.d.ts +433 -0
  161. package/dist/plus/integrations/providers/models.d.ts.map +1 -0
  162. package/dist/plus/integrations/providers/models.js +602 -0
  163. package/dist/plus/integrations/providers/models.js.map +1 -0
  164. package/dist/plus/integrations/providers/providersApi.d.ts +129 -0
  165. package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -0
  166. package/dist/plus/integrations/providers/providersApi.js +602 -0
  167. package/dist/plus/integrations/providers/providersApi.js.map +1 -0
  168. package/dist/plus/integrations/providers/utils.d.ts +54 -0
  169. package/dist/plus/integrations/providers/utils.d.ts.map +1 -0
  170. package/dist/plus/integrations/providers/utils.js +262 -0
  171. package/dist/plus/integrations/providers/utils.js.map +1 -0
  172. package/dist/plus/integrations/telemetry.d.ts +26 -0
  173. package/dist/plus/integrations/telemetry.d.ts.map +1 -0
  174. package/dist/plus/integrations/telemetry.js +11 -0
  175. package/dist/plus/integrations/telemetry.js.map +1 -0
  176. package/dist/plus/integrations/utils/integration.utils.d.ts +14 -0
  177. package/dist/plus/integrations/utils/integration.utils.d.ts.map +1 -0
  178. package/dist/plus/integrations/utils/integration.utils.js +111 -0
  179. package/dist/plus/integrations/utils/integration.utils.js.map +1 -0
  180. package/dist/utils/disposable.d.ts +15 -2
  181. package/dist/utils/disposable.d.ts.map +1 -1
  182. package/dist/utils/disposable.js.map +1 -1
  183. package/package.json +43 -2
  184. package/src/git/context.ts +4 -3
  185. package/src/git/providers/operations.ts +1 -1
  186. package/src/git/providers/worktrees.ts +14 -2
  187. package/src/git/utils/conflictResolution.utils.ts +126 -0
  188. package/src/git-cli/providers/operations.ts +22 -2
  189. package/src/git-cli/providers/worktrees.ts +17 -2
  190. package/src/plus/ai/utils/ai.utils.ts +5 -5
  191. package/src/plus/git-github/api/github.ts +13 -25
  192. package/src/plus/integrations/authentication/cloudIntegrationService.ts +120 -0
  193. package/src/plus/integrations/authentication/configuredIntegrationService.ts +414 -0
  194. package/src/plus/integrations/authentication/integrationAuthenticationProvider.ts +289 -0
  195. package/src/plus/integrations/authentication/integrationAuthenticationService.ts +144 -0
  196. package/src/plus/integrations/authentication/manualTokenProvider.ts +72 -0
  197. package/src/plus/integrations/authentication/models.ts +168 -0
  198. package/src/plus/integrations/constants.ts +169 -0
  199. package/src/plus/integrations/context.ts +383 -0
  200. package/src/plus/integrations/errors.ts +35 -0
  201. package/src/plus/integrations/index.ts +119 -0
  202. package/src/plus/integrations/integrationService.ts +924 -0
  203. package/src/plus/integrations/models/gitHostIntegration.ts +738 -0
  204. package/src/plus/integrations/models/integration.ts +657 -0
  205. package/src/plus/integrations/models/issuesIntegration.ts +124 -0
  206. package/src/plus/integrations/providers/apiClients.ts +72 -0
  207. package/src/plus/integrations/providers/azure/azure.ts +691 -0
  208. package/src/plus/integrations/providers/azure/models.ts +605 -0
  209. package/src/plus/integrations/providers/azureDevOps.ts +677 -0
  210. package/src/plus/integrations/providers/bitbucket/bitbucket.ts +774 -0
  211. package/src/plus/integrations/providers/bitbucket/models.ts +445 -0
  212. package/src/plus/integrations/providers/bitbucket-server/models.ts +252 -0
  213. package/src/plus/integrations/providers/bitbucket-server.ts +282 -0
  214. package/src/plus/integrations/providers/bitbucket.ts +350 -0
  215. package/src/plus/integrations/providers/github/github.ts +64 -0
  216. package/src/plus/integrations/providers/github/github.utils.ts +23 -0
  217. package/src/plus/integrations/providers/github/models.ts +29 -0
  218. package/src/plus/integrations/providers/github.ts +366 -0
  219. package/src/plus/integrations/providers/gitlab/gitlab.ts +1210 -0
  220. package/src/plus/integrations/providers/gitlab/gitlab.utils.ts +71 -0
  221. package/src/plus/integrations/providers/gitlab/models.ts +257 -0
  222. package/src/plus/integrations/providers/gitlab.ts +473 -0
  223. package/src/plus/integrations/providers/jira.ts +359 -0
  224. package/src/plus/integrations/providers/linear.ts +275 -0
  225. package/src/plus/integrations/providers/models.ts +1114 -0
  226. package/src/plus/integrations/providers/providersApi.ts +1126 -0
  227. package/src/plus/integrations/providers/utils.ts +359 -0
  228. package/src/plus/integrations/telemetry.ts +29 -0
  229. package/src/plus/integrations/utils/integration.utils.ts +138 -0
  230. 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
+ }