@theia/ai-copilot 1.75.0 → 1.76.0-next.10

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 (150) hide show
  1. package/README.md +86 -14
  2. package/lib/browser/copilot-auth-dialog.d.ts.map +1 -1
  3. package/lib/browser/copilot-auth-dialog.js +12 -4
  4. package/lib/browser/copilot-auth-dialog.js.map +1 -1
  5. package/lib/browser/copilot-frontend-application-contribution.d.ts +15 -1
  6. package/lib/browser/copilot-frontend-application-contribution.d.ts.map +1 -1
  7. package/lib/browser/copilot-frontend-application-contribution.js +52 -24
  8. package/lib/browser/copilot-frontend-application-contribution.js.map +1 -1
  9. package/lib/common/copilot-auth-service.d.ts +27 -24
  10. package/lib/common/copilot-auth-service.d.ts.map +1 -1
  11. package/lib/common/copilot-language-models-manager.d.ts +3 -21
  12. package/lib/common/copilot-language-models-manager.d.ts.map +1 -1
  13. package/lib/common/copilot-language-models-manager.js +1 -13
  14. package/lib/common/copilot-language-models-manager.js.map +1 -1
  15. package/lib/common/copilot-preferences.d.ts +1 -0
  16. package/lib/common/copilot-preferences.d.ts.map +1 -1
  17. package/lib/common/copilot-preferences.js +24 -6
  18. package/lib/common/copilot-preferences.js.map +1 -1
  19. package/lib/common/index.d.ts +0 -1
  20. package/lib/common/index.d.ts.map +1 -1
  21. package/lib/common/index.js +0 -1
  22. package/lib/common/index.js.map +1 -1
  23. package/lib/node/copilot-auth-service-impl.d.ts +25 -16
  24. package/lib/node/copilot-auth-service-impl.d.ts.map +1 -1
  25. package/lib/node/copilot-auth-service-impl.js +67 -183
  26. package/lib/node/copilot-auth-service-impl.js.map +1 -1
  27. package/lib/node/copilot-auth-service-impl.spec.js +104 -168
  28. package/lib/node/copilot-auth-service-impl.spec.js.map +1 -1
  29. package/lib/node/copilot-backend-module.d.ts.map +1 -1
  30. package/lib/node/copilot-backend-module.js +10 -2
  31. package/lib/node/copilot-backend-module.js.map +1 -1
  32. package/lib/node/copilot-cli-auth-provider.d.ts +146 -0
  33. package/lib/node/copilot-cli-auth-provider.d.ts.map +1 -0
  34. package/lib/node/copilot-cli-auth-provider.js +430 -0
  35. package/lib/node/copilot-cli-auth-provider.js.map +1 -0
  36. package/lib/node/copilot-cli-auth-provider.spec.d.ts +2 -0
  37. package/lib/node/copilot-cli-auth-provider.spec.d.ts.map +1 -0
  38. package/lib/node/copilot-cli-auth-provider.spec.js +249 -0
  39. package/lib/node/copilot-cli-auth-provider.spec.js.map +1 -0
  40. package/lib/node/copilot-cli-locator.d.ts +120 -0
  41. package/lib/node/copilot-cli-locator.d.ts.map +1 -0
  42. package/lib/node/copilot-cli-locator.js +348 -0
  43. package/lib/node/copilot-cli-locator.js.map +1 -0
  44. package/lib/node/copilot-cli-locator.spec.d.ts +2 -0
  45. package/lib/node/copilot-cli-locator.spec.d.ts.map +1 -0
  46. package/lib/node/copilot-cli-locator.spec.js +327 -0
  47. package/lib/node/copilot-cli-locator.spec.js.map +1 -0
  48. package/lib/node/copilot-credential-store.d.ts +45 -0
  49. package/lib/node/copilot-credential-store.d.ts.map +1 -0
  50. package/lib/node/copilot-credential-store.js +96 -0
  51. package/lib/node/copilot-credential-store.js.map +1 -0
  52. package/lib/node/copilot-credential-store.spec.d.ts +2 -0
  53. package/lib/node/copilot-credential-store.spec.d.ts.map +1 -0
  54. package/lib/node/copilot-credential-store.spec.js +93 -0
  55. package/lib/node/copilot-credential-store.spec.js.map +1 -0
  56. package/lib/node/copilot-language-models-manager-impl.d.ts +20 -18
  57. package/lib/node/copilot-language-models-manager-impl.d.ts.map +1 -1
  58. package/lib/node/copilot-language-models-manager-impl.js +39 -67
  59. package/lib/node/copilot-language-models-manager-impl.js.map +1 -1
  60. package/lib/node/copilot-language-models-manager-impl.spec.d.ts +2 -0
  61. package/lib/node/copilot-language-models-manager-impl.spec.d.ts.map +1 -0
  62. package/lib/node/copilot-language-models-manager-impl.spec.js +119 -0
  63. package/lib/node/copilot-language-models-manager-impl.spec.js.map +1 -0
  64. package/lib/node/copilot-runtime-env.d.ts +20 -0
  65. package/lib/node/copilot-runtime-env.d.ts.map +1 -0
  66. package/lib/node/copilot-runtime-env.js +49 -0
  67. package/lib/node/copilot-runtime-env.js.map +1 -0
  68. package/lib/node/copilot-sdk-client-provider.d.ts +103 -0
  69. package/lib/node/copilot-sdk-client-provider.d.ts.map +1 -0
  70. package/lib/node/copilot-sdk-client-provider.js +247 -0
  71. package/lib/node/copilot-sdk-client-provider.js.map +1 -0
  72. package/lib/node/copilot-sdk-client-provider.spec.d.ts +2 -0
  73. package/lib/node/copilot-sdk-client-provider.spec.d.ts.map +1 -0
  74. package/lib/node/copilot-sdk-client-provider.spec.js +215 -0
  75. package/lib/node/copilot-sdk-client-provider.spec.js.map +1 -0
  76. package/lib/node/copilot-sdk-language-model.d.ts +85 -0
  77. package/lib/node/copilot-sdk-language-model.d.ts.map +1 -0
  78. package/lib/node/copilot-sdk-language-model.js +301 -0
  79. package/lib/node/copilot-sdk-language-model.js.map +1 -0
  80. package/lib/node/copilot-sdk-language-model.spec.d.ts +2 -0
  81. package/lib/node/copilot-sdk-language-model.spec.d.ts.map +1 -0
  82. package/lib/node/copilot-sdk-language-model.spec.js +302 -0
  83. package/lib/node/copilot-sdk-language-model.spec.js.map +1 -0
  84. package/lib/node/copilot-sdk-loader.d.ts +57 -0
  85. package/lib/node/copilot-sdk-loader.d.ts.map +1 -0
  86. package/lib/node/copilot-sdk-loader.js +133 -0
  87. package/lib/node/copilot-sdk-loader.js.map +1 -0
  88. package/lib/node/copilot-sdk-loader.spec.d.ts +2 -0
  89. package/lib/node/copilot-sdk-loader.spec.d.ts.map +1 -0
  90. package/lib/node/copilot-sdk-loader.spec.js +113 -0
  91. package/lib/node/copilot-sdk-loader.spec.js.map +1 -0
  92. package/lib/node/copilot-sdk-mappers.d.ts +49 -0
  93. package/lib/node/copilot-sdk-mappers.d.ts.map +1 -0
  94. package/lib/node/copilot-sdk-mappers.js +147 -0
  95. package/lib/node/copilot-sdk-mappers.js.map +1 -0
  96. package/lib/node/copilot-sdk-mappers.spec.d.ts +2 -0
  97. package/lib/node/copilot-sdk-mappers.spec.d.ts.map +1 -0
  98. package/lib/node/copilot-sdk-mappers.spec.js +133 -0
  99. package/lib/node/copilot-sdk-mappers.spec.js.map +1 -0
  100. package/lib/node/copilot-sdk-types.d.ts +198 -0
  101. package/lib/node/copilot-sdk-types.d.ts.map +1 -0
  102. package/lib/{common/copilot-oauth-config.js → node/copilot-sdk-types.js} +2 -13
  103. package/lib/node/copilot-sdk-types.js.map +1 -0
  104. package/lib/node/index.d.ts +9 -1
  105. package/lib/node/index.d.ts.map +1 -1
  106. package/lib/node/index.js +9 -1
  107. package/lib/node/index.js.map +1 -1
  108. package/package.json +4 -6
  109. package/src/browser/copilot-auth-dialog.tsx +12 -8
  110. package/src/browser/copilot-frontend-application-contribution.ts +62 -29
  111. package/src/common/copilot-auth-service.ts +28 -24
  112. package/src/common/copilot-language-models-manager.ts +3 -28
  113. package/src/common/copilot-preferences.ts +24 -5
  114. package/src/common/index.ts +0 -1
  115. package/src/node/copilot-auth-service-impl.spec.ts +122 -207
  116. package/src/node/copilot-auth-service-impl.ts +63 -218
  117. package/src/node/copilot-backend-module.ts +11 -2
  118. package/src/node/copilot-cli-auth-provider.spec.ts +315 -0
  119. package/src/node/copilot-cli-auth-provider.ts +444 -0
  120. package/src/node/copilot-cli-locator.spec.ts +386 -0
  121. package/src/node/copilot-cli-locator.ts +363 -0
  122. package/src/node/copilot-credential-store.spec.ts +106 -0
  123. package/src/node/copilot-credential-store.ts +99 -0
  124. package/src/node/copilot-language-models-manager-impl.spec.ts +137 -0
  125. package/src/node/copilot-language-models-manager-impl.ts +51 -85
  126. package/src/node/copilot-runtime-env.ts +46 -0
  127. package/src/node/copilot-sdk-client-provider.spec.ts +253 -0
  128. package/src/node/copilot-sdk-client-provider.ts +241 -0
  129. package/src/node/copilot-sdk-language-model.spec.ts +363 -0
  130. package/src/node/copilot-sdk-language-model.ts +329 -0
  131. package/src/node/copilot-sdk-loader.spec.ts +126 -0
  132. package/src/node/copilot-sdk-loader.ts +142 -0
  133. package/src/node/copilot-sdk-mappers.spec.ts +151 -0
  134. package/src/node/copilot-sdk-mappers.ts +161 -0
  135. package/src/node/copilot-sdk-types.ts +276 -0
  136. package/src/node/index.ts +9 -1
  137. package/lib/common/copilot-oauth-config.d.ts +0 -33
  138. package/lib/common/copilot-oauth-config.d.ts.map +0 -1
  139. package/lib/common/copilot-oauth-config.js.map +0 -1
  140. package/lib/node/copilot-language-model.d.ts +0 -34
  141. package/lib/node/copilot-language-model.d.ts.map +0 -1
  142. package/lib/node/copilot-language-model.js +0 -236
  143. package/lib/node/copilot-language-model.js.map +0 -1
  144. package/lib/node/copilot-language-model.spec.d.ts +0 -2
  145. package/lib/node/copilot-language-model.spec.d.ts.map +0 -1
  146. package/lib/node/copilot-language-model.spec.js +0 -89
  147. package/lib/node/copilot-language-model.spec.js.map +0 -1
  148. package/src/common/copilot-oauth-config.ts +0 -55
  149. package/src/node/copilot-language-model.spec.ts +0 -106
  150. package/src/node/copilot-language-model.ts +0 -273
@@ -14,50 +14,56 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { inject, injectable, named } from '@theia/core/shared/inversify';
17
+ import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
18
18
  import { Emitter, Event, ILogger } from '@theia/core';
19
- import { KeyStoreService } from '@theia/core/lib/common/key-store';
20
19
  import {
21
20
  CopilotAuthService,
22
21
  CopilotAuthServiceClient,
23
22
  CopilotAuthState,
24
23
  DeviceCodeResponse
25
24
  } from '../common/copilot-auth-service';
26
- import { CopilotOAuthConfig } from '../common/copilot-oauth-config';
27
-
28
- const COPILOT_SCOPE = 'read:user';
29
- const COPILOT_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
25
+ import { CopilotCliAuthProvider } from './copilot-cli-auth-provider';
26
+ import { CopilotCliLocator } from './copilot-cli-locator';
27
+ import { CopilotCredentialStore } from './copilot-credential-store';
30
28
 
31
29
  /**
32
- * Maximum number of polling attempts for token retrieval.
33
- * With a default 5-second interval, this allows approximately 5 minutes of polling.
34
- */
35
- const MAX_POLLING_ATTEMPTS = 60;
36
-
37
- interface StoredCredentials {
38
- accessToken: string;
39
- accountLabel?: string;
40
- enterpriseUrl?: string;
41
- }
42
-
43
- /**
44
- * Backend implementation of the GitHub Copilot OAuth Device Flow authentication service.
45
- * Handles device code generation, token polling, and credential storage.
30
+ * Backend implementation of the GitHub Copilot authentication service.
31
+ *
32
+ * All Copilot requests are served by the official Copilot CLI, which owns its credentials, so this
33
+ * service only drives the sign-in of the CLI and reports its state, see {@link CopilotCliAuthProvider}.
46
34
  */
47
35
  @injectable()
48
36
  export class CopilotAuthServiceImpl implements CopilotAuthService {
49
37
 
50
- @inject(KeyStoreService)
51
- protected readonly keyStoreService: KeyStoreService;
38
+ @inject(CopilotCliAuthProvider)
39
+ protected readonly cliAuthProvider: CopilotCliAuthProvider;
40
+
41
+ @inject(CopilotCredentialStore)
42
+ protected readonly credentialStore: CopilotCredentialStore;
43
+
44
+ @inject(CopilotCliLocator)
45
+ protected readonly cliLocator: CopilotCliLocator;
52
46
 
53
47
  @inject(ILogger) @named('ai-copilot:CopilotAuthServiceImpl')
54
48
  protected readonly logger: ILogger;
55
49
 
56
- @inject(CopilotOAuthConfig)
57
- protected readonly oauthConfig: CopilotOAuthConfig;
58
-
59
50
  protected client: CopilotAuthServiceClient | undefined;
60
51
  protected cachedState: CopilotAuthState | undefined;
52
+ protected migrationRequired = false;
53
+ protected legacyCleanup: Promise<void>;
54
+
55
+ @postConstruct()
56
+ protected init(): void {
57
+ // The previous integration signed in with an OAuth application of its own and stored a token
58
+ // that is of no use here. Remove it instead of leaving it on the machine of the user.
59
+ this.legacyCleanup = this.credentialStore.deleteLegacy().then(async removed => {
60
+ this.migrationRequired = removed;
61
+ if (removed) {
62
+ this.logger.info('Copilot: discarded the sign-in of the previous integration, a new sign-in is required.');
63
+ this.updateAuthState(await this.computeAuthState());
64
+ }
65
+ }, error => this.logger.warn('Copilot: failed to clean up the previous sign-in:', error));
66
+ }
61
67
 
62
68
  protected readonly onAuthStateChangedEmitter = new Emitter<CopilotAuthState>();
63
69
  readonly onAuthStateChanged: Event<CopilotAuthState> = this.onAuthStateChangedEmitter.event;
@@ -66,210 +72,53 @@ export class CopilotAuthServiceImpl implements CopilotAuthService {
66
72
  this.client = client;
67
73
  }
68
74
 
69
- protected getOAuthEndpoints(enterpriseUrl?: string): { deviceCodeUrl: string; accessTokenUrl: string } {
70
- if (enterpriseUrl) {
71
- const domain = enterpriseUrl
72
- .replace(/^https?:\/\//, '')
73
- .replace(/\/$/, '');
74
- return {
75
- deviceCodeUrl: `https://${domain}/login/device/code`,
76
- accessTokenUrl: `https://${domain}/login/oauth/access_token`
77
- };
78
- }
79
- return {
80
- deviceCodeUrl: 'https://github.com/login/device/code',
81
- accessTokenUrl: 'https://github.com/login/oauth/access_token'
82
- };
75
+ async setExecutablePath(path: string | undefined): Promise<void> {
76
+ this.cliLocator.setConfiguredPath(path);
83
77
  }
84
78
 
85
- async initiateDeviceFlow(enterpriseUrl?: string): Promise<DeviceCodeResponse> {
86
- const endpoints = this.getOAuthEndpoints(enterpriseUrl);
87
-
88
- const response = await fetch(endpoints.deviceCodeUrl, {
89
- method: 'POST',
90
- headers: {
91
- 'Accept': 'application/json',
92
- 'Content-Type': 'application/json',
93
- 'User-Agent': this.oauthConfig.userAgent
94
- },
95
- body: JSON.stringify({
96
- client_id: this.oauthConfig.clientId,
97
- scope: COPILOT_SCOPE
98
- })
99
- });
100
-
101
- if (!response.ok) {
102
- const errorText = await response.text();
103
- throw new Error(`Failed to initiate device authorization: ${response.status} - ${errorText}`);
104
- }
105
-
106
- const data = await response.json() as DeviceCodeResponse;
107
- return data;
79
+ async startSignIn(enterpriseUrl?: string): Promise<DeviceCodeResponse> {
80
+ return this.cliAuthProvider.startLogin(enterpriseUrl);
108
81
  }
109
82
 
110
- async pollForToken(deviceCode: string, interval: number, enterpriseUrl?: string): Promise<boolean> {
111
- const endpoints = this.getOAuthEndpoints(enterpriseUrl);
112
- let attempts = 0;
113
-
114
- while (attempts < MAX_POLLING_ATTEMPTS) {
115
- await this.delay(interval * 1000);
116
- attempts++;
117
-
118
- const response = await fetch(endpoints.accessTokenUrl, {
119
- method: 'POST',
120
- headers: {
121
- 'Accept': 'application/json',
122
- 'Content-Type': 'application/json',
123
- 'User-Agent': this.oauthConfig.userAgent
124
- },
125
- body: JSON.stringify({
126
- client_id: this.oauthConfig.clientId,
127
- device_code: deviceCode,
128
- grant_type: COPILOT_GRANT_TYPE
129
- })
130
- });
131
-
132
- if (!response.ok) {
133
- this.logger.error(`Token request failed: ${response.status}`);
134
- continue;
135
- }
136
-
137
- const data = await response.json() as {
138
- access_token?: string;
139
- error?: string;
140
- error_description?: string;
141
- };
142
-
143
- if (data.access_token) {
144
- // Get user info for account label
145
- const accountLabel = await this.fetchAccountLabel(data.access_token, enterpriseUrl);
146
-
147
- // Store credentials
148
- const credentials: StoredCredentials = {
149
- accessToken: data.access_token,
150
- accountLabel,
151
- enterpriseUrl
152
- };
153
-
154
- await this.keyStoreService.setPassword(
155
- this.oauthConfig.keystoreService,
156
- this.oauthConfig.keystoreAccount,
157
- JSON.stringify(credentials)
158
- );
159
-
160
- // Update cached state and notify
161
- const newState: CopilotAuthState = {
162
- isAuthenticated: true,
163
- accountLabel,
164
- enterpriseUrl
165
- };
166
- this.updateAuthState(newState);
167
-
168
- return true;
169
- }
170
-
171
- if (data.error === 'authorization_pending') {
172
- // User hasn't authorized yet, continue polling
173
- continue;
174
- }
175
-
176
- if (data.error === 'slow_down') {
177
- // Increase polling interval
178
- interval += 5;
179
- continue;
180
- }
181
-
182
- if (data.error === 'expired_token' || data.error === 'access_denied') {
183
- this.logger.error(`Authorization failed: ${data.error} - ${data.error_description}`);
184
- return false;
185
- }
186
-
187
- if (data.error) {
188
- this.logger.error(`Unexpected error: ${data.error} - ${data.error_description}`);
189
- return false;
190
- }
83
+ async waitForSignIn(): Promise<boolean> {
84
+ const success = await this.cliAuthProvider.waitForLogin();
85
+ if (success) {
86
+ this.cachedState = undefined;
87
+ this.updateAuthState(await this.cliAuthProvider.getAuthState());
191
88
  }
192
-
193
- return false;
89
+ return success;
194
90
  }
195
91
 
196
- protected async fetchAccountLabel(accessToken: string, enterpriseUrl?: string): Promise<string | undefined> {
197
- try {
198
- const apiBaseUrl = enterpriseUrl
199
- ? `https://${enterpriseUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')}/api/v3`
200
- : 'https://api.github.com';
201
-
202
- const response = await fetch(`${apiBaseUrl}/user`, {
203
- headers: {
204
- 'Authorization': `Bearer ${accessToken}`,
205
- 'User-Agent': this.oauthConfig.userAgent,
206
- 'Accept': 'application/vnd.github.v3+json'
207
- }
208
- });
92
+ async cancelSignIn(): Promise<void> {
93
+ await this.cliAuthProvider.cancelLogin();
94
+ }
209
95
 
210
- if (response.ok) {
211
- const userData = await response.json() as { login?: string };
212
- return userData.login;
213
- }
214
- } catch (error) {
215
- this.logger.warn('Failed to fetch GitHub user info:', error);
216
- }
217
- return undefined;
96
+ async signOut(): Promise<void> {
97
+ await this.cliAuthProvider.signOut();
98
+ this.cachedState = undefined;
99
+ this.updateAuthState(await this.getAuthState());
218
100
  }
219
101
 
220
102
  async getAuthState(): Promise<CopilotAuthState> {
221
- if (this.cachedState) {
222
- return this.cachedState;
103
+ // Awaited so that a state requested early cannot be cached without the migration flag, which
104
+ // is the only thing that tells the user why they were signed out. The frontend evaluates it
105
+ // once at startup, so losing it there means losing it altogether.
106
+ await this.legacyCleanup;
107
+ if (!this.cachedState) {
108
+ this.cachedState = await this.computeAuthState();
223
109
  }
224
-
225
- try {
226
- const stored = await this.keyStoreService.getPassword(this.oauthConfig.keystoreService, this.oauthConfig.keystoreAccount);
227
- if (stored) {
228
- const credentials: StoredCredentials = JSON.parse(stored);
229
- // Tokens from the current OAuth App start with 'gho_'; other prefixes (e.g. 'ghu_') indicate a token from the previous GitHub App (Iv-prefixed client ID).
230
- if (!credentials.accessToken.startsWith('gho_')) {
231
- this.logger.info('Copilot: clearing outdated GitHub App token. Please sign in again.');
232
- await this.keyStoreService.deletePassword(this.oauthConfig.keystoreService, this.oauthConfig.keystoreAccount);
233
- this.cachedState = { isAuthenticated: false, migrationRequired: true };
234
- return this.cachedState;
235
- }
236
- this.cachedState = {
237
- isAuthenticated: true,
238
- accountLabel: credentials.accountLabel,
239
- enterpriseUrl: credentials.enterpriseUrl
240
- };
241
- return this.cachedState;
242
- }
243
- } catch (error) {
244
- this.logger.warn('Failed to retrieve Copilot credentials:', error);
245
- }
246
-
247
- this.cachedState = { isAuthenticated: false };
248
110
  return this.cachedState;
249
111
  }
250
112
 
251
- async getAccessToken(): Promise<string | undefined> {
252
- try {
253
- const stored = await this.keyStoreService.getPassword(this.oauthConfig.keystoreService, this.oauthConfig.keystoreAccount);
254
- if (stored) {
255
- const credentials: StoredCredentials = JSON.parse(stored);
256
- return credentials.accessToken;
257
- }
258
- } catch (error) {
259
- this.logger.warn('Failed to retrieve Copilot access token:', error);
260
- }
261
- return undefined;
262
- }
263
-
264
- async signOut(): Promise<void> {
265
- try {
266
- await this.keyStoreService.deletePassword(this.oauthConfig.keystoreService, this.oauthConfig.keystoreAccount);
267
- } catch (error) {
268
- this.logger.warn('Failed to delete Copilot credentials:', error);
269
- }
270
-
271
- const newState: CopilotAuthState = { isAuthenticated: false };
272
- this.updateAuthState(newState);
113
+ /**
114
+ * The state as it currently is, with the request for a new sign-in attached when the credentials
115
+ * of the previous integration had to be discarded and none have been established since.
116
+ *
117
+ * Does not await the cleanup, so that it can be used from it.
118
+ */
119
+ protected async computeAuthState(): Promise<CopilotAuthState> {
120
+ const state = await this.cliAuthProvider.getAuthState();
121
+ return state.isAuthenticated || !this.migrationRequired ? state : { ...state, migrationRequired: true };
273
122
  }
274
123
 
275
124
  protected updateAuthState(state: CopilotAuthState): void {
@@ -277,8 +126,4 @@ export class CopilotAuthServiceImpl implements CopilotAuthService {
277
126
  this.onAuthStateChangedEmitter.fire(state);
278
127
  this.client?.onAuthStateChanged(state);
279
128
  }
280
-
281
- protected delay(ms: number): Promise<void> {
282
- return new Promise(resolve => setTimeout(resolve, ms));
283
- }
284
129
  }
@@ -24,11 +24,21 @@ import {
24
24
  COPILOT_AUTH_SERVICE_PATH,
25
25
  CopilotAuthServiceClient
26
26
  } from '../common';
27
- import { CopilotOAuthConfig, DEFAULT_COPILOT_OAUTH_CONFIG } from '../common/copilot-oauth-config';
28
27
  import { CopilotLanguageModelsManagerImpl } from './copilot-language-models-manager-impl';
28
+ import { CopilotSdkClientProvider } from './copilot-sdk-client-provider';
29
+ import { CopilotSdkLoader } from './copilot-sdk-loader';
30
+ import { CopilotCliAuthProvider } from './copilot-cli-auth-provider';
31
+ import { CopilotCliLocator } from './copilot-cli-locator';
32
+ import { CopilotCredentialStore } from './copilot-credential-store';
29
33
  import { CopilotAuthServiceImpl } from './copilot-auth-service-impl';
30
34
 
31
35
  const copilotConnectionModule = ConnectionContainerModule.create(({ bind }) => {
36
+ bind(CopilotCliLocator).toSelf().inSingletonScope();
37
+ bind(CopilotSdkLoader).toSelf().inSingletonScope();
38
+ bind(CopilotCredentialStore).toSelf().inSingletonScope();
39
+ bind(CopilotSdkClientProvider).toSelf().inSingletonScope();
40
+ bind(CopilotCliAuthProvider).toSelf().inSingletonScope();
41
+
32
42
  bind(CopilotAuthServiceImpl).toSelf().inSingletonScope();
33
43
  bind(CopilotAuthService).toService(CopilotAuthServiceImpl);
34
44
 
@@ -55,6 +65,5 @@ const copilotConnectionModule = ConnectionContainerModule.create(({ bind }) => {
55
65
  });
56
66
 
57
67
  export default new ContainerModule(bind => {
58
- bind(CopilotOAuthConfig).toConstantValue(DEFAULT_COPILOT_OAUTH_CONFIG);
59
68
  bind(ConnectionContainerModule).toConstantValue(copilotConnectionModule);
60
69
  });
@@ -0,0 +1,315 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { promises as fs } from 'fs';
19
+ import * as os from 'os';
20
+ import { join } from 'path';
21
+ import { Deferred } from '@theia/core/lib/common/promise-util';
22
+ import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
23
+ import { DeviceCodeResponse } from '../common/copilot-auth-service';
24
+ import { CopilotCliAuthProvider } from './copilot-cli-auth-provider';
25
+
26
+ class TestableCopilotCliAuthProvider extends CopilotCliAuthProvider {
27
+ constructor() {
28
+ super();
29
+ // The logger is injected in production, and read-only for everyone else.
30
+ Object.assign(this, { logger: new MockLogger() });
31
+ }
32
+
33
+ callParseDeviceCode(output: string): DeviceCodeResponse | undefined {
34
+ return this.parseDeviceCode(output);
35
+ }
36
+
37
+ callFindStoredToken(home: string): Promise<string | undefined> {
38
+ return this.findStoredToken(home);
39
+ }
40
+
41
+ callFindTokenInText(text: string): string | undefined {
42
+ return this.findTokenInText(text);
43
+ }
44
+
45
+ callParseAccountLabel(output: string): string | undefined {
46
+ return this.parseAccountLabel(output);
47
+ }
48
+
49
+ callCreateLoginEnv(home: string, noopBrowser: string): Record<string, string | undefined> {
50
+ return this.createLoginEnv(home, noopBrowser);
51
+ }
52
+
53
+ callExtractFailure(output: string): string | undefined {
54
+ return this.extractFailure(output);
55
+ }
56
+
57
+ callToHostUrl(enterpriseUrl: string): string {
58
+ return this.toHostUrl(enterpriseUrl);
59
+ }
60
+
61
+ setLoginResult(result: Deferred<boolean>): void {
62
+ Object.assign(this, { loginResult: result });
63
+ }
64
+ }
65
+
66
+ describe('CopilotCliAuthProvider - parseDeviceCode', () => {
67
+
68
+ const provider = new TestableCopilotCliAuthProvider();
69
+
70
+ it('should extract from the output format of the current CLI', () => {
71
+ const parsed = provider.callParseDeviceCode(
72
+ 'To authenticate, visit https://github.com/login/device and enter code B54F-40A3\nWaiting for authorization...\n'
73
+ );
74
+ expect(parsed?.verification_uri).to.equal('https://github.com/login/device');
75
+ expect(parsed?.user_code).to.equal('B54F-40A3');
76
+ });
77
+
78
+ it('should extract the verification URL and the user code', () => {
79
+ const parsed = provider.callParseDeviceCode(
80
+ 'Please enter the code AAEB-CD8C at https://github.com/login/device to authenticate.\n'
81
+ );
82
+ expect(parsed?.verification_uri).to.equal('https://github.com/login/device');
83
+ expect(parsed?.user_code).to.equal('AAEB-CD8C');
84
+ });
85
+
86
+ it('should extract from output spread across several lines', () => {
87
+ const parsed = provider.callParseDeviceCode([
88
+ 'Opening your browser to authenticate...',
89
+ 'First copy your one-time code: 1A2B-3C4D',
90
+ 'Then open https://github.com/login/device in your browser.'
91
+ ].join('\n'));
92
+ expect(parsed?.user_code).to.equal('1A2B-3C4D');
93
+ expect(parsed?.verification_uri).to.equal('https://github.com/login/device');
94
+ });
95
+
96
+ it('should extract an enterprise verification URL', () => {
97
+ const parsed = provider.callParseDeviceCode(
98
+ 'Enter WXYZ-7890 at https://company.ghe.com/login/device\n'
99
+ );
100
+ expect(parsed?.verification_uri).to.equal('https://company.ghe.com/login/device');
101
+ expect(parsed?.user_code).to.equal('WXYZ-7890');
102
+ });
103
+
104
+ it('should return undefined while only the URL has been reported', () => {
105
+ expect(provider.callParseDeviceCode('Visit https://github.com/login/device\n')).to.be.undefined;
106
+ });
107
+
108
+ it('should return undefined while only the code has been reported', () => {
109
+ expect(provider.callParseDeviceCode('Your code is AAEB-CD8C\n')).to.be.undefined;
110
+ });
111
+
112
+ it('should return undefined for unrelated output', () => {
113
+ expect(provider.callParseDeviceCode('Opening your browser to authenticate...\n')).to.be.undefined;
114
+ });
115
+
116
+ it('should not mistake a lowercase word pair for a code', () => {
117
+ expect(provider.callParseDeviceCode('see also-here at https://github.com/login/device\n')).to.be.undefined;
118
+ });
119
+ });
120
+
121
+ describe('CopilotCliAuthProvider - extractFailure', () => {
122
+
123
+ const provider = new TestableCopilotCliAuthProvider();
124
+
125
+ it('should pick the line reporting the failure', () => {
126
+ const failure = provider.callExtractFailure([
127
+ 'Waiting for authorization...',
128
+ 'Error: Access denied by policy settings (Request ID: DEA8:572C1)'
129
+ ].join('\n'));
130
+ expect(failure).to.equal('Error: Access denied by policy settings (Request ID: DEA8:572C1)');
131
+ });
132
+
133
+ it('should pick the last failure line when several are reported', () => {
134
+ const failure = provider.callExtractFailure([
135
+ 'Error: first problem',
136
+ 'unauthorized: not authorized to use this Copilot feature'
137
+ ].join('\n'));
138
+ expect(failure).to.equal('unauthorized: not authorized to use this Copilot feature');
139
+ });
140
+
141
+ it('should ignore the clipboard failure the CLI reports on a healthy login', () => {
142
+ const failure = provider.callExtractFailure([
143
+ 'To authenticate, visit https://github.com/login/device and enter code B54F-40A3',
144
+ 'Failed to copy to clipboard. Please visit https://github.com/login/device and enter the code manually.',
145
+ 'Error: Access denied by policy settings'
146
+ ].join('\n'));
147
+ expect(failure).to.equal('Error: Access denied by policy settings');
148
+ });
149
+
150
+ it('should not report the clipboard failure as the cause when it is the only match', () => {
151
+ const failure = provider.callExtractFailure([
152
+ 'Waiting for authorization...',
153
+ 'Failed to copy to clipboard. Please visit https://github.com/login/device manually.'
154
+ ].join('\n'));
155
+ expect(failure).to.contain('Waiting for authorization');
156
+ expect(failure).to.not.contain('clipboard');
157
+ });
158
+
159
+ it('should fall back to the tail of the output when no line names the problem', () => {
160
+ expect(provider.callExtractFailure('Waiting for authorization...\n')).to.contain('Waiting for authorization');
161
+ });
162
+ });
163
+
164
+ describe('CopilotCliAuthProvider - toHostUrl', () => {
165
+
166
+ const provider = new TestableCopilotCliAuthProvider();
167
+
168
+ it('should prefix a bare domain with https', () => {
169
+ expect(provider.callToHostUrl('company.ghe.com')).to.equal('https://company.ghe.com');
170
+ });
171
+
172
+ it('should keep a URL that already has a scheme', () => {
173
+ expect(provider.callToHostUrl('https://company.ghe.com')).to.equal('https://company.ghe.com');
174
+ });
175
+
176
+ it('should upgrade a plain HTTP URL, since the sign-in carries credentials', () => {
177
+ expect(provider.callToHostUrl('http://company.ghe.com')).to.equal('https://company.ghe.com');
178
+ });
179
+
180
+ it('should not mistake a host that merely starts with "http" for a URL with a scheme', () => {
181
+ expect(provider.callToHostUrl('httpbin.example.com')).to.equal('https://httpbin.example.com');
182
+ });
183
+ });
184
+
185
+ describe('CopilotCliAuthProvider - cancelLogin', () => {
186
+
187
+ it('should settle a login that is being awaited, so that closing the dialog does not hang it', async () => {
188
+ const provider = new TestableCopilotCliAuthProvider();
189
+ const result = new Deferred<boolean>();
190
+ provider.setLoginResult(result);
191
+
192
+ await provider.cancelLogin();
193
+
194
+ expect(await result.promise).to.equal(false);
195
+ });
196
+ });
197
+
198
+ describe('CopilotCliAuthProvider - findTokenInText', () => {
199
+
200
+ const provider = new TestableCopilotCliAuthProvider();
201
+
202
+ it('should find the token in the commented JSON the CLI writes', () => {
203
+ const config = [
204
+ '// User settings written by the Copilot CLI',
205
+ '{',
206
+ ' "github": { "token": "gho_abcdefghijklmnopqrstuvwxyz0123" },',
207
+ ' "host": "https://github.com"',
208
+ '}'
209
+ ].join('\n');
210
+ expect(provider.callFindTokenInText(config)).to.equal('gho_abcdefghijklmnopqrstuvwxyz0123');
211
+ });
212
+
213
+ it('should find a token regardless of the surrounding format', () => {
214
+ expect(provider.callFindTokenInText('token = ghu_abcdefghijklmnopqrstuvwxyz0123\n')).to.equal('ghu_abcdefghijklmnopqrstuvwxyz0123');
215
+ });
216
+
217
+ it('should not match values that only look similar', () => {
218
+ expect(provider.callFindTokenInText('{ "user": "ndoschek", "short": "gho_tooshort", "url": "https://github.com" }')).to.be.undefined;
219
+ });
220
+
221
+ it('should return undefined for content without a token', () => {
222
+ expect(provider.callFindTokenInText('// nothing to see here\n{}\n')).to.be.undefined;
223
+ });
224
+
225
+ });
226
+
227
+ describe('CopilotCliAuthProvider - findStoredToken', () => {
228
+
229
+ const provider = new TestableCopilotCliAuthProvider();
230
+ const token = 'gho_abcdefghijklmnopqrstuvwxyz0123';
231
+ let home: string;
232
+
233
+ beforeEach(async () => {
234
+ home = await fs.mkdtemp(join(os.tmpdir(), 'theia-copilot-token-'));
235
+ });
236
+
237
+ afterEach(async () => {
238
+ await fs.rm(home, { recursive: true, force: true });
239
+ });
240
+
241
+ it('should find the token the CLI persisted', async () => {
242
+ await fs.writeFile(join(home, 'config.json'), `{ "github": { "token": "${token}" } }`);
243
+ expect(await provider.callFindStoredToken(home)).to.equal(token);
244
+ });
245
+
246
+ it('should not inspect the files this provider wrote itself', async () => {
247
+ await fs.writeFile(join(home, 'settings.json'), `{ "token": "${token}" }`);
248
+ await fs.writeFile(join(home, 'no-browser.sh'), `#!/bin/sh\n# ${token}\n`);
249
+ expect(await provider.callFindStoredToken(home)).to.be.undefined;
250
+ });
251
+
252
+ it('should skip a file that is implausibly large instead of reading it', async () => {
253
+ // Written with a leading newline so that the token would be found if the file were read.
254
+ await fs.writeFile(join(home, 'huge.json'), 'x'.repeat(1024 * 1024 + 1) + `\n${token}\n`);
255
+ expect(await provider.callFindStoredToken(home)).to.be.undefined;
256
+ });
257
+
258
+ it('should return undefined for a home without a token', async () => {
259
+ await fs.writeFile(join(home, 'config.json'), '{ "host": "https://github.com" }');
260
+ expect(await provider.callFindStoredToken(home)).to.be.undefined;
261
+ });
262
+ });
263
+
264
+ describe('CopilotCliAuthProvider - parseAccountLabel', () => {
265
+
266
+ const provider = new TestableCopilotCliAuthProvider();
267
+
268
+ it('should extract the login the CLI reported', () => {
269
+ expect(provider.callParseAccountLabel('Signed in successfully as ndoschek.')).to.equal('ndoschek');
270
+ });
271
+
272
+ it('should extract the login without a trailing period', () => {
273
+ expect(provider.callParseAccountLabel('Signed in successfully as some-user\n')).to.equal('some-user');
274
+ });
275
+
276
+ it('should return undefined when no login was reported', () => {
277
+ expect(provider.callParseAccountLabel('Waiting for authorization...')).to.be.undefined;
278
+ });
279
+ });
280
+
281
+ describe('CopilotCliAuthProvider - createLoginEnv', () => {
282
+
283
+ const provider = new TestableCopilotCliAuthProvider();
284
+
285
+ it('should isolate the login in the given home with the keychain disabled', () => {
286
+ const env = provider.callCreateLoginEnv('/tmp/theia-copilot-login-test', '/tmp/theia-copilot-login-test/no-browser.sh');
287
+ expect(env.COPILOT_HOME).to.equal('/tmp/theia-copilot-login-test');
288
+ expect(env.COPILOT_DISABLE_KEYTAR).to.equal('1');
289
+ });
290
+
291
+ it('should remove a token variable whatever its case, since Windows ignores it', () => {
292
+ const previous = { ...process.env };
293
+ try {
294
+ process.env.Github_Token = 'gho_environment';
295
+ const env = provider.callCreateLoginEnv('/tmp/home', '/tmp/home/no-browser.sh');
296
+ expect(Object.keys(env).some(name => name.toLowerCase() === 'github_token')).to.be.false;
297
+ } finally {
298
+ process.env = previous;
299
+ }
300
+ });
301
+
302
+ it('should remove tokens from the environment so they cannot satisfy the sign-in', () => {
303
+ const env = provider.callCreateLoginEnv('/tmp/theia-copilot-login-test', '/tmp/theia-copilot-login-test/no-browser.sh');
304
+ expect(env.COPILOT_GITHUB_TOKEN).to.be.undefined;
305
+ expect(env.GH_TOKEN).to.be.undefined;
306
+ expect(env.GITHUB_TOKEN).to.be.undefined;
307
+ });
308
+
309
+ it('should point the CLI at the no-op opener and drop the display variables', () => {
310
+ const env = provider.callCreateLoginEnv('/tmp/theia-copilot-login-test', '/tmp/theia-copilot-login-test/no-browser.sh');
311
+ expect(env.BROWSER).to.equal('/tmp/theia-copilot-login-test/no-browser.sh');
312
+ expect(env.DISPLAY).to.be.undefined;
313
+ expect(env.WAYLAND_DISPLAY).to.be.undefined;
314
+ });
315
+ });