@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.
- package/README.md +86 -14
- package/lib/browser/copilot-auth-dialog.d.ts.map +1 -1
- package/lib/browser/copilot-auth-dialog.js +12 -4
- package/lib/browser/copilot-auth-dialog.js.map +1 -1
- package/lib/browser/copilot-frontend-application-contribution.d.ts +15 -1
- package/lib/browser/copilot-frontend-application-contribution.d.ts.map +1 -1
- package/lib/browser/copilot-frontend-application-contribution.js +52 -24
- package/lib/browser/copilot-frontend-application-contribution.js.map +1 -1
- package/lib/common/copilot-auth-service.d.ts +27 -24
- package/lib/common/copilot-auth-service.d.ts.map +1 -1
- package/lib/common/copilot-language-models-manager.d.ts +3 -21
- package/lib/common/copilot-language-models-manager.d.ts.map +1 -1
- package/lib/common/copilot-language-models-manager.js +1 -13
- package/lib/common/copilot-language-models-manager.js.map +1 -1
- package/lib/common/copilot-preferences.d.ts +1 -0
- package/lib/common/copilot-preferences.d.ts.map +1 -1
- package/lib/common/copilot-preferences.js +24 -6
- package/lib/common/copilot-preferences.js.map +1 -1
- package/lib/common/index.d.ts +0 -1
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +0 -1
- package/lib/common/index.js.map +1 -1
- package/lib/node/copilot-auth-service-impl.d.ts +25 -16
- package/lib/node/copilot-auth-service-impl.d.ts.map +1 -1
- package/lib/node/copilot-auth-service-impl.js +67 -183
- package/lib/node/copilot-auth-service-impl.js.map +1 -1
- package/lib/node/copilot-auth-service-impl.spec.js +104 -168
- package/lib/node/copilot-auth-service-impl.spec.js.map +1 -1
- package/lib/node/copilot-backend-module.d.ts.map +1 -1
- package/lib/node/copilot-backend-module.js +10 -2
- package/lib/node/copilot-backend-module.js.map +1 -1
- package/lib/node/copilot-cli-auth-provider.d.ts +146 -0
- package/lib/node/copilot-cli-auth-provider.d.ts.map +1 -0
- package/lib/node/copilot-cli-auth-provider.js +430 -0
- package/lib/node/copilot-cli-auth-provider.js.map +1 -0
- package/lib/node/copilot-cli-auth-provider.spec.d.ts +2 -0
- package/lib/node/copilot-cli-auth-provider.spec.d.ts.map +1 -0
- package/lib/node/copilot-cli-auth-provider.spec.js +249 -0
- package/lib/node/copilot-cli-auth-provider.spec.js.map +1 -0
- package/lib/node/copilot-cli-locator.d.ts +120 -0
- package/lib/node/copilot-cli-locator.d.ts.map +1 -0
- package/lib/node/copilot-cli-locator.js +348 -0
- package/lib/node/copilot-cli-locator.js.map +1 -0
- package/lib/node/copilot-cli-locator.spec.d.ts +2 -0
- package/lib/node/copilot-cli-locator.spec.d.ts.map +1 -0
- package/lib/node/copilot-cli-locator.spec.js +327 -0
- package/lib/node/copilot-cli-locator.spec.js.map +1 -0
- package/lib/node/copilot-credential-store.d.ts +45 -0
- package/lib/node/copilot-credential-store.d.ts.map +1 -0
- package/lib/node/copilot-credential-store.js +96 -0
- package/lib/node/copilot-credential-store.js.map +1 -0
- package/lib/node/copilot-credential-store.spec.d.ts +2 -0
- package/lib/node/copilot-credential-store.spec.d.ts.map +1 -0
- package/lib/node/copilot-credential-store.spec.js +93 -0
- package/lib/node/copilot-credential-store.spec.js.map +1 -0
- package/lib/node/copilot-language-models-manager-impl.d.ts +20 -18
- package/lib/node/copilot-language-models-manager-impl.d.ts.map +1 -1
- package/lib/node/copilot-language-models-manager-impl.js +39 -67
- package/lib/node/copilot-language-models-manager-impl.js.map +1 -1
- package/lib/node/copilot-language-models-manager-impl.spec.d.ts +2 -0
- package/lib/node/copilot-language-models-manager-impl.spec.d.ts.map +1 -0
- package/lib/node/copilot-language-models-manager-impl.spec.js +119 -0
- package/lib/node/copilot-language-models-manager-impl.spec.js.map +1 -0
- package/lib/node/copilot-runtime-env.d.ts +20 -0
- package/lib/node/copilot-runtime-env.d.ts.map +1 -0
- package/lib/node/copilot-runtime-env.js +49 -0
- package/lib/node/copilot-runtime-env.js.map +1 -0
- package/lib/node/copilot-sdk-client-provider.d.ts +103 -0
- package/lib/node/copilot-sdk-client-provider.d.ts.map +1 -0
- package/lib/node/copilot-sdk-client-provider.js +247 -0
- package/lib/node/copilot-sdk-client-provider.js.map +1 -0
- package/lib/node/copilot-sdk-client-provider.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-client-provider.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-client-provider.spec.js +215 -0
- package/lib/node/copilot-sdk-client-provider.spec.js.map +1 -0
- package/lib/node/copilot-sdk-language-model.d.ts +85 -0
- package/lib/node/copilot-sdk-language-model.d.ts.map +1 -0
- package/lib/node/copilot-sdk-language-model.js +301 -0
- package/lib/node/copilot-sdk-language-model.js.map +1 -0
- package/lib/node/copilot-sdk-language-model.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-language-model.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-language-model.spec.js +302 -0
- package/lib/node/copilot-sdk-language-model.spec.js.map +1 -0
- package/lib/node/copilot-sdk-loader.d.ts +57 -0
- package/lib/node/copilot-sdk-loader.d.ts.map +1 -0
- package/lib/node/copilot-sdk-loader.js +133 -0
- package/lib/node/copilot-sdk-loader.js.map +1 -0
- package/lib/node/copilot-sdk-loader.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-loader.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-loader.spec.js +113 -0
- package/lib/node/copilot-sdk-loader.spec.js.map +1 -0
- package/lib/node/copilot-sdk-mappers.d.ts +49 -0
- package/lib/node/copilot-sdk-mappers.d.ts.map +1 -0
- package/lib/node/copilot-sdk-mappers.js +147 -0
- package/lib/node/copilot-sdk-mappers.js.map +1 -0
- package/lib/node/copilot-sdk-mappers.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-mappers.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-mappers.spec.js +133 -0
- package/lib/node/copilot-sdk-mappers.spec.js.map +1 -0
- package/lib/node/copilot-sdk-types.d.ts +198 -0
- package/lib/node/copilot-sdk-types.d.ts.map +1 -0
- package/lib/{common/copilot-oauth-config.js → node/copilot-sdk-types.js} +2 -13
- package/lib/node/copilot-sdk-types.js.map +1 -0
- package/lib/node/index.d.ts +9 -1
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +9 -1
- package/lib/node/index.js.map +1 -1
- package/package.json +4 -6
- package/src/browser/copilot-auth-dialog.tsx +12 -8
- package/src/browser/copilot-frontend-application-contribution.ts +62 -29
- package/src/common/copilot-auth-service.ts +28 -24
- package/src/common/copilot-language-models-manager.ts +3 -28
- package/src/common/copilot-preferences.ts +24 -5
- package/src/common/index.ts +0 -1
- package/src/node/copilot-auth-service-impl.spec.ts +122 -207
- package/src/node/copilot-auth-service-impl.ts +63 -218
- package/src/node/copilot-backend-module.ts +11 -2
- package/src/node/copilot-cli-auth-provider.spec.ts +315 -0
- package/src/node/copilot-cli-auth-provider.ts +444 -0
- package/src/node/copilot-cli-locator.spec.ts +386 -0
- package/src/node/copilot-cli-locator.ts +363 -0
- package/src/node/copilot-credential-store.spec.ts +106 -0
- package/src/node/copilot-credential-store.ts +99 -0
- package/src/node/copilot-language-models-manager-impl.spec.ts +137 -0
- package/src/node/copilot-language-models-manager-impl.ts +51 -85
- package/src/node/copilot-runtime-env.ts +46 -0
- package/src/node/copilot-sdk-client-provider.spec.ts +253 -0
- package/src/node/copilot-sdk-client-provider.ts +241 -0
- package/src/node/copilot-sdk-language-model.spec.ts +363 -0
- package/src/node/copilot-sdk-language-model.ts +329 -0
- package/src/node/copilot-sdk-loader.spec.ts +126 -0
- package/src/node/copilot-sdk-loader.ts +142 -0
- package/src/node/copilot-sdk-mappers.spec.ts +151 -0
- package/src/node/copilot-sdk-mappers.ts +161 -0
- package/src/node/copilot-sdk-types.ts +276 -0
- package/src/node/index.ts +9 -1
- package/lib/common/copilot-oauth-config.d.ts +0 -33
- package/lib/common/copilot-oauth-config.d.ts.map +0 -1
- package/lib/common/copilot-oauth-config.js.map +0 -1
- package/lib/node/copilot-language-model.d.ts +0 -34
- package/lib/node/copilot-language-model.d.ts.map +0 -1
- package/lib/node/copilot-language-model.js +0 -236
- package/lib/node/copilot-language-model.js.map +0 -1
- package/lib/node/copilot-language-model.spec.d.ts +0 -2
- package/lib/node/copilot-language-model.spec.d.ts.map +0 -1
- package/lib/node/copilot-language-model.spec.js +0 -89
- package/lib/node/copilot-language-model.spec.js.map +0 -1
- package/src/common/copilot-oauth-config.ts +0 -55
- package/src/node/copilot-language-model.spec.ts +0 -106
- package/src/node/copilot-language-model.ts +0 -273
|
@@ -21,19 +21,13 @@ export const CopilotAuthService = Symbol('CopilotAuthService');
|
|
|
21
21
|
export const CopilotAuthServiceClient = Symbol('CopilotAuthServiceClient');
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* The device code flow information reported by the Copilot CLI for the UI to display.
|
|
25
25
|
*/
|
|
26
26
|
export interface DeviceCodeResponse {
|
|
27
27
|
/** URL where user should enter the code (e.g., https://github.com/login/device) */
|
|
28
28
|
verification_uri: string;
|
|
29
29
|
/** Code to display to the user (e.g., XXXX-XXXX) */
|
|
30
30
|
user_code: string;
|
|
31
|
-
/** Device code used for polling */
|
|
32
|
-
device_code: string;
|
|
33
|
-
/** Polling interval in seconds */
|
|
34
|
-
interval: number;
|
|
35
|
-
/** Expiration time in seconds */
|
|
36
|
-
expires_in: number;
|
|
37
31
|
}
|
|
38
32
|
|
|
39
33
|
/**
|
|
@@ -46,7 +40,10 @@ export interface CopilotAuthState {
|
|
|
46
40
|
accountLabel?: string;
|
|
47
41
|
/** GitHub Enterprise URL if using enterprise */
|
|
48
42
|
enterpriseUrl?: string;
|
|
49
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* True when the credentials of a previous version were found and discarded, so that the user can
|
|
45
|
+
* be told why they have to sign in again.
|
|
46
|
+
*/
|
|
50
47
|
migrationRequired?: boolean;
|
|
51
48
|
}
|
|
52
49
|
|
|
@@ -58,41 +55,48 @@ export interface CopilotAuthServiceClient {
|
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
/**
|
|
61
|
-
* Service for
|
|
58
|
+
* Service for signing the GitHub Copilot CLI in, which serves all Copilot requests.
|
|
59
|
+
*
|
|
60
|
+
* The sign-in is a device code flow that the CLI runs and polls itself. This service starts it and
|
|
61
|
+
* reports its progress, so that it can be presented like any other device code flow.
|
|
62
62
|
*/
|
|
63
63
|
export interface CopilotAuthService {
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* Tells the backend where the Copilot CLI is, as configured by the user.
|
|
66
|
+
*
|
|
67
|
+
* The CLI is not shipped with the application and has to be found on the machine hosting the
|
|
68
|
+
* backend, which cannot read the preferences of the user itself.
|
|
69
|
+
* @param path Location of the CLI executable, or `undefined` to search for it
|
|
68
70
|
*/
|
|
69
|
-
|
|
71
|
+
setExecutablePath(path: string | undefined): Promise<void>;
|
|
70
72
|
|
|
71
73
|
/**
|
|
72
|
-
*
|
|
73
|
-
* @param deviceCode The device code from initiateDeviceFlow
|
|
74
|
-
* @param interval Polling interval in seconds
|
|
74
|
+
* Starts the sign-in and resolves once the code the user has to enter is known.
|
|
75
75
|
* @param enterpriseUrl Optional GitHub Enterprise domain
|
|
76
|
-
* @returns true if authentication succeeded, false if expired/denied
|
|
77
76
|
*/
|
|
78
|
-
|
|
77
|
+
startSignIn(enterpriseUrl?: string): Promise<DeviceCodeResponse>;
|
|
79
78
|
|
|
80
79
|
/**
|
|
81
|
-
*
|
|
80
|
+
* Waits for the sign-in started by {@link startSignIn} to complete.
|
|
81
|
+
* @returns true if the sign-in succeeded, false if it was denied or expired
|
|
82
82
|
*/
|
|
83
|
-
|
|
83
|
+
waitForSignIn(): Promise<boolean>;
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
*
|
|
87
|
-
* @returns The access token or undefined if not authenticated
|
|
86
|
+
* Aborts a sign-in that is still in progress, if any.
|
|
88
87
|
*/
|
|
89
|
-
|
|
88
|
+
cancelSignIn(): Promise<void>;
|
|
90
89
|
|
|
91
90
|
/**
|
|
92
|
-
*
|
|
91
|
+
* Discards the stored credentials of this application.
|
|
93
92
|
*/
|
|
94
93
|
signOut(): Promise<void>;
|
|
95
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Get the current authentication state.
|
|
97
|
+
*/
|
|
98
|
+
getAuthState(): Promise<CopilotAuthState>;
|
|
99
|
+
|
|
96
100
|
/**
|
|
97
101
|
* Set the client to receive auth state change notifications.
|
|
98
102
|
*/
|
|
@@ -18,18 +18,6 @@ export const COPILOT_LANGUAGE_MODELS_MANAGER_PATH = '/services/copilot/language-
|
|
|
18
18
|
export const CopilotLanguageModelsManager = Symbol('CopilotLanguageModelsManager');
|
|
19
19
|
|
|
20
20
|
export const COPILOT_PROVIDER_ID = 'copilot';
|
|
21
|
-
export const COPILOT_API_BASE_URL = 'https://api.githubcopilot.com';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Returns the Copilot API base URL, taking an optional GitHub Enterprise domain into account.
|
|
25
|
-
*/
|
|
26
|
-
export function getCopilotApiBaseUrl(enterpriseUrl?: string): string {
|
|
27
|
-
if (enterpriseUrl) {
|
|
28
|
-
const domain = enterpriseUrl.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
29
|
-
return `https://copilot-api.${domain}`;
|
|
30
|
-
}
|
|
31
|
-
return COPILOT_API_BASE_URL;
|
|
32
|
-
}
|
|
33
21
|
|
|
34
22
|
export interface CopilotModelDescription {
|
|
35
23
|
/**
|
|
@@ -38,17 +26,9 @@ export interface CopilotModelDescription {
|
|
|
38
26
|
*/
|
|
39
27
|
id: string;
|
|
40
28
|
/**
|
|
41
|
-
* The model ID as used by the Copilot API (e.g., 'gpt-
|
|
29
|
+
* The model ID as used by the Copilot API (e.g., 'gpt-5.5', 'claude-sonnet-5').
|
|
42
30
|
*/
|
|
43
31
|
model: string;
|
|
44
|
-
/**
|
|
45
|
-
* Indicate whether the streaming API shall be used.
|
|
46
|
-
*/
|
|
47
|
-
enableStreaming: boolean;
|
|
48
|
-
/**
|
|
49
|
-
* Flag to configure whether the model supports structured output.
|
|
50
|
-
*/
|
|
51
|
-
supportsStructuredOutput: boolean;
|
|
52
32
|
/**
|
|
53
33
|
* Maximum number of retry attempts when a request fails.
|
|
54
34
|
*/
|
|
@@ -64,18 +44,13 @@ export interface CopilotLanguageModelsManager {
|
|
|
64
44
|
* Remove language models from the registry.
|
|
65
45
|
*/
|
|
66
46
|
removeLanguageModels(...modelIds: string[]): void;
|
|
67
|
-
/**
|
|
68
|
-
* Set the GitHub Enterprise URL for Copilot API requests.
|
|
69
|
-
*/
|
|
70
|
-
setEnterpriseUrl(url: string | undefined): void;
|
|
71
47
|
/**
|
|
72
48
|
* Refresh the status of all Copilot models (e.g., after authentication state changes).
|
|
73
49
|
*/
|
|
74
50
|
refreshModelsStatus(): Promise<void>;
|
|
75
51
|
/**
|
|
76
|
-
* Fetches the list of available model IDs from the Copilot
|
|
77
|
-
* Requires
|
|
78
|
-
* or if the API call fails.
|
|
52
|
+
* Fetches the list of available model IDs from the Copilot CLI.
|
|
53
|
+
* Requires the CLI to be signed in. Returns an empty array otherwise or if the call fails.
|
|
79
54
|
*/
|
|
80
55
|
fetchAvailableModelIds(): Promise<string[]>;
|
|
81
56
|
}
|
|
@@ -20,6 +20,7 @@ import { nls, PreferenceSchema } from '@theia/core';
|
|
|
20
20
|
export const COPILOT_ENABLED_PREF = 'ai-features.copilot.enabled';
|
|
21
21
|
export const COPILOT_MODEL_OVERRIDES_PREF = 'ai-features.copilot.modelOverrides';
|
|
22
22
|
export const COPILOT_ENTERPRISE_URL_PREF = 'ai-features.copilot.enterpriseUrl';
|
|
23
|
+
export const COPILOT_EXECUTABLE_PATH_PREF = 'ai-features.copilot.executablePath';
|
|
23
24
|
|
|
24
25
|
export const CopilotPreferencesSchema: PreferenceSchema = {
|
|
25
26
|
properties: {
|
|
@@ -28,9 +29,13 @@ export const CopilotPreferencesSchema: PreferenceSchema = {
|
|
|
28
29
|
typeDetails: { [MODEL_PROVIDER_TYPE_DETAIL]: { label: 'GitHub Copilot' } satisfies ModelProviderTypeDetail },
|
|
29
30
|
markdownDescription: nls.localize('theia/ai/copilot/enabled/mdDescription',
|
|
30
31
|
'Enable the GitHub Copilot provider. When enabled, a status bar entry '
|
|
31
|
-
+ 'appears for authentication and available models are discovered from your Copilot subscription
|
|
32
|
+
+ 'appears for authentication and available models are discovered from your Copilot subscription.\n\n'
|
|
33
|
+
+ 'Requests are routed through the official GitHub Copilot CLI, which runs as a background process on the '
|
|
34
|
+
+ 'machine hosting the backend. It is given the credentials of your Copilot sign-in and nothing else, so a '
|
|
35
|
+
+ 'token in the environment or an existing sign-in of the GitHub CLI is not used.'),
|
|
32
36
|
title: AI_CORE_PREFERENCES_TITLE,
|
|
33
|
-
default: true
|
|
37
|
+
default: true,
|
|
38
|
+
tags: ['experimental']
|
|
34
39
|
},
|
|
35
40
|
[COPILOT_MODEL_OVERRIDES_PREF]: {
|
|
36
41
|
type: 'array',
|
|
@@ -42,14 +47,28 @@ export const CopilotPreferencesSchema: PreferenceSchema = {
|
|
|
42
47
|
default: [],
|
|
43
48
|
items: {
|
|
44
49
|
type: 'string'
|
|
45
|
-
}
|
|
50
|
+
},
|
|
51
|
+
tags: ['experimental']
|
|
46
52
|
},
|
|
47
53
|
[COPILOT_ENTERPRISE_URL_PREF]: {
|
|
48
54
|
type: 'string',
|
|
49
55
|
markdownDescription: nls.localize('theia/ai/copilot/enterpriseUrl/mdDescription',
|
|
50
|
-
'GitHub Enterprise domain
|
|
56
|
+
'GitHub Enterprise domain to authenticate and send requests against (e.g., `github.mycompany.com`). '
|
|
57
|
+
+ 'Leave empty for GitHub.com.'),
|
|
51
58
|
title: AI_CORE_PREFERENCES_TITLE,
|
|
52
|
-
default: ''
|
|
59
|
+
default: '',
|
|
60
|
+
tags: ['experimental']
|
|
61
|
+
},
|
|
62
|
+
[COPILOT_EXECUTABLE_PATH_PREF]: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
markdownDescription: nls.localize('theia/ai/copilot/executablePath/mdDescription',
|
|
65
|
+
'Location of the GitHub Copilot CLI executable on the machine hosting the backend. '
|
|
66
|
+
+ 'When empty (default), the CLI is searched for in the installation of the application, on the `PATH` '
|
|
67
|
+
+ 'and in the global `npm` directory. Set this when the CLI is installed somewhere else.\n\n'
|
|
68
|
+
+ 'The CLI is not shipped with the application, install it with `npm install -g @github/copilot`.'),
|
|
69
|
+
title: AI_CORE_PREFERENCES_TITLE,
|
|
70
|
+
default: '',
|
|
71
|
+
tags: ['experimental']
|
|
53
72
|
}
|
|
54
73
|
}
|
|
55
74
|
};
|
package/src/common/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// *****************************************************************************
|
|
2
|
-
// Copyright (C) 2026 EclipseSource and others.
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
3
3
|
//
|
|
4
4
|
// This program and the accompanying materials are made available under the
|
|
5
5
|
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
@@ -15,235 +15,150 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { expect } from 'chai';
|
|
18
|
-
import * as sinon from 'sinon';
|
|
19
|
-
import { Container } from '@theia/core/shared/inversify';
|
|
20
|
-
import { KeyStoreService } from '@theia/core/lib/common/key-store';
|
|
21
|
-
import { CopilotOAuthConfig, DEFAULT_COPILOT_OAUTH_CONFIG } from '../common/copilot-oauth-config';
|
|
22
|
-
import { CopilotAuthServiceImpl } from './copilot-auth-service-impl';
|
|
23
|
-
import { ILogger } from '@theia/core/lib/common/logger';
|
|
24
18
|
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
|
|
19
|
+
import { CopilotAuthState } from '../common/copilot-auth-service';
|
|
20
|
+
import { CopilotAuthServiceImpl } from './copilot-auth-service-impl';
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
container.bind(CopilotAuthServiceImpl).toSelf().inSingletonScope();
|
|
48
|
-
container.bind(ILogger).to(MockLogger).inSingletonScope();
|
|
49
|
-
|
|
50
|
-
authService = container.get(CopilotAuthServiceImpl);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
afterEach(() => {
|
|
54
|
-
sinon.restore();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('getAccessToken', () => {
|
|
58
|
-
|
|
59
|
-
it('should return undefined when no credentials are stored', async () => {
|
|
60
|
-
getPasswordStub.resolves(undefined);
|
|
61
|
-
|
|
62
|
-
const token = await authService.getAccessToken();
|
|
63
|
-
expect(token).to.be.undefined;
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should return the stored access token directly', async () => {
|
|
67
|
-
const oauthToken = 'ghu_test_token';
|
|
68
|
-
|
|
69
|
-
getPasswordStub.resolves(JSON.stringify({
|
|
70
|
-
accessToken: oauthToken,
|
|
71
|
-
accountLabel: 'testuser'
|
|
72
|
-
}));
|
|
73
|
-
|
|
74
|
-
const token = await authService.getAccessToken();
|
|
75
|
-
expect(token).to.equal(oauthToken);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should return undefined when stored data is malformed', async () => {
|
|
79
|
-
getPasswordStub.resolves('not-valid-json');
|
|
22
|
+
interface Stubs {
|
|
23
|
+
/** Whether the sign-in of the previous integration had to be discarded. */
|
|
24
|
+
legacyRemoved: boolean;
|
|
25
|
+
/** How long that cleanup takes, to model a slow keychain. */
|
|
26
|
+
legacyDelay: number;
|
|
27
|
+
authState: CopilotAuthState;
|
|
28
|
+
authStateCalls: number;
|
|
29
|
+
configuredPath: string | undefined;
|
|
30
|
+
deleted: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class TestableCopilotAuthServiceImpl extends CopilotAuthServiceImpl {
|
|
34
|
+
|
|
35
|
+
readonly stubs: Stubs = {
|
|
36
|
+
legacyRemoved: false,
|
|
37
|
+
legacyDelay: 0,
|
|
38
|
+
authState: { isAuthenticated: false },
|
|
39
|
+
authStateCalls: 0,
|
|
40
|
+
configuredPath: undefined,
|
|
41
|
+
deleted: 0
|
|
42
|
+
};
|
|
80
43
|
|
|
81
|
-
|
|
82
|
-
|
|
44
|
+
constructor() {
|
|
45
|
+
super();
|
|
46
|
+
const stubs = this.stubs;
|
|
47
|
+
Object.assign(this, {
|
|
48
|
+
logger: new MockLogger(),
|
|
49
|
+
credentialStore: {
|
|
50
|
+
deleteLegacy: async () => {
|
|
51
|
+
await new Promise(resolve => setTimeout(resolve, stubs.legacyDelay));
|
|
52
|
+
return stubs.legacyRemoved;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
cliLocator: { setConfiguredPath: (path: string | undefined) => { stubs.configuredPath = path; } },
|
|
56
|
+
cliAuthProvider: {
|
|
57
|
+
getAuthState: async () => {
|
|
58
|
+
stubs.authStateCalls++;
|
|
59
|
+
return stubs.authState;
|
|
60
|
+
},
|
|
61
|
+
signOut: async () => {
|
|
62
|
+
stubs.deleted++;
|
|
63
|
+
stubs.authState = { isAuthenticated: false };
|
|
64
|
+
},
|
|
65
|
+
waitForLogin: async () => true
|
|
66
|
+
}
|
|
83
67
|
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Runs what `@postConstruct` runs in production. */
|
|
71
|
+
start(): void {
|
|
72
|
+
this.init();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('CopilotAuthServiceImpl - migration of the previous sign-in', () => {
|
|
77
|
+
|
|
78
|
+
it('should report that a new sign-in is required, even when asked before the cleanup finished', async () => {
|
|
79
|
+
const service = new TestableCopilotAuthServiceImpl();
|
|
80
|
+
service.stubs.legacyRemoved = true;
|
|
81
|
+
// The frontend evaluates the flag once at startup, so a state cached without it loses it.
|
|
82
|
+
service.stubs.legacyDelay = 20;
|
|
83
|
+
service.start();
|
|
84
|
+
expect(await service.getAuthState()).to.deep.include({ isAuthenticated: false, migrationRequired: true });
|
|
84
85
|
});
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const state = await authService.getAuthState();
|
|
92
|
-
expect(state.isAuthenticated).to.be.false;
|
|
93
|
-
expect(state.accountLabel).to.be.undefined;
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should return authenticated with account label when credentials exist', async () => {
|
|
97
|
-
getPasswordStub.resolves(JSON.stringify({
|
|
98
|
-
accessToken: 'gho_test',
|
|
99
|
-
accountLabel: 'testuser',
|
|
100
|
-
enterpriseUrl: undefined
|
|
101
|
-
}));
|
|
102
|
-
|
|
103
|
-
const state = await authService.getAuthState();
|
|
104
|
-
expect(state.isAuthenticated).to.be.true;
|
|
105
|
-
expect(state.accountLabel).to.equal('testuser');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should return enterprise URL when stored', async () => {
|
|
109
|
-
getPasswordStub.resolves(JSON.stringify({
|
|
110
|
-
accessToken: 'gho_test',
|
|
111
|
-
accountLabel: 'testuser',
|
|
112
|
-
enterpriseUrl: 'github.mycompany.com'
|
|
113
|
-
}));
|
|
114
|
-
|
|
115
|
-
const state = await authService.getAuthState();
|
|
116
|
-
expect(state.isAuthenticated).to.be.true;
|
|
117
|
-
expect(state.enterpriseUrl).to.equal('github.mycompany.com');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should clear outdated non-gho_ token and flag migration', async () => {
|
|
121
|
-
getPasswordStub.resolves(JSON.stringify({
|
|
122
|
-
accessToken: 'ghu_old_token',
|
|
123
|
-
accountLabel: 'testuser'
|
|
124
|
-
}));
|
|
125
|
-
|
|
126
|
-
const state = await authService.getAuthState();
|
|
127
|
-
expect(state.isAuthenticated).to.be.false;
|
|
128
|
-
expect(state.migrationRequired).to.be.true;
|
|
129
|
-
expect((keyStoreService.deletePassword as sinon.SinonStub).calledOnce).to.be.true;
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should cache the auth state after first retrieval', async () => {
|
|
133
|
-
getPasswordStub.resolves(JSON.stringify({
|
|
134
|
-
accessToken: 'gho_test',
|
|
135
|
-
accountLabel: 'testuser'
|
|
136
|
-
}));
|
|
137
|
-
|
|
138
|
-
const state1 = await authService.getAuthState();
|
|
139
|
-
const state2 = await authService.getAuthState();
|
|
140
|
-
|
|
141
|
-
expect(state1).to.equal(state2);
|
|
142
|
-
expect(getPasswordStub.calledOnce).to.be.true;
|
|
143
|
-
});
|
|
87
|
+
it('should not claim a migration when there was nothing to discard', async () => {
|
|
88
|
+
const service = new TestableCopilotAuthServiceImpl();
|
|
89
|
+
service.start();
|
|
90
|
+
expect(await service.getAuthState()).to.not.have.property('migrationRequired');
|
|
144
91
|
});
|
|
145
92
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const stateBefore = await authService.getAuthState();
|
|
155
|
-
expect(stateBefore.isAuthenticated).to.be.true;
|
|
156
|
-
|
|
157
|
-
await authService.signOut();
|
|
158
|
-
|
|
159
|
-
// After sign out, cached state should be cleared
|
|
160
|
-
// getAuthState should now return the new state without hitting keystore cache
|
|
161
|
-
const stateAfter = await authService.getAuthState();
|
|
162
|
-
expect(stateAfter.isAuthenticated).to.be.false;
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should fire auth state changed event on sign out', async () => {
|
|
166
|
-
const stateChanges: { isAuthenticated: boolean }[] = [];
|
|
167
|
-
authService.onAuthStateChanged(state => stateChanges.push(state));
|
|
168
|
-
|
|
169
|
-
await authService.signOut();
|
|
170
|
-
|
|
171
|
-
expect(stateChanges).to.have.lengthOf(1);
|
|
172
|
-
expect(stateChanges[0].isAuthenticated).to.be.false;
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should delete credentials from keystore on sign out', async () => {
|
|
176
|
-
await authService.signOut();
|
|
177
|
-
expect((keyStoreService.deletePassword as sinon.SinonStub).calledOnce).to.be.true;
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should notify client on sign out', async () => {
|
|
181
|
-
const client = { onAuthStateChanged: sinon.stub() };
|
|
182
|
-
authService.setClient(client);
|
|
183
|
-
|
|
184
|
-
await authService.signOut();
|
|
93
|
+
it('should not claim a migration for a user who is signed in', async () => {
|
|
94
|
+
const service = new TestableCopilotAuthServiceImpl();
|
|
95
|
+
service.stubs.legacyRemoved = true;
|
|
96
|
+
service.stubs.authState = { isAuthenticated: true, accountLabel: 'octocat' };
|
|
97
|
+
service.start();
|
|
98
|
+
expect(await service.getAuthState()).to.not.have.property('migrationRequired');
|
|
99
|
+
});
|
|
185
100
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
});
|
|
101
|
+
it('should survive a keychain that cannot be read', async () => {
|
|
102
|
+
const service = new TestableCopilotAuthServiceImpl();
|
|
103
|
+
Object.assign(service, { credentialStore: { deleteLegacy: async () => { throw new Error('keychain unavailable'); } } });
|
|
104
|
+
service.start();
|
|
105
|
+
expect(await service.getAuthState()).to.deep.equal({ isAuthenticated: false });
|
|
189
106
|
});
|
|
190
107
|
});
|
|
191
108
|
|
|
192
|
-
describe('CopilotAuthServiceImpl
|
|
193
|
-
|
|
194
|
-
const customConfig: CopilotOAuthConfig = {
|
|
195
|
-
clientId: 'CustomClientId',
|
|
196
|
-
userAgent: 'MyApp/1.0.0',
|
|
197
|
-
keystoreService: 'myapp-copilot',
|
|
198
|
-
keystoreAccount: 'myapp-github'
|
|
199
|
-
};
|
|
109
|
+
describe('CopilotAuthServiceImpl - state', () => {
|
|
200
110
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
111
|
+
it('should ask the provider once and serve the cached state afterwards', async () => {
|
|
112
|
+
const service = new TestableCopilotAuthServiceImpl();
|
|
113
|
+
service.start();
|
|
114
|
+
await service.getAuthState();
|
|
115
|
+
await service.getAuthState();
|
|
116
|
+
expect(service.stubs.authStateCalls).to.equal(1);
|
|
117
|
+
});
|
|
204
118
|
|
|
205
|
-
|
|
206
|
-
const
|
|
119
|
+
it('should report the new state after a sign-in', async () => {
|
|
120
|
+
const service = new TestableCopilotAuthServiceImpl();
|
|
121
|
+
service.start();
|
|
122
|
+
await service.getAuthState();
|
|
123
|
+
service.stubs.authState = { isAuthenticated: true, accountLabel: 'octocat' };
|
|
207
124
|
|
|
208
|
-
|
|
125
|
+
const reported: CopilotAuthState[] = [];
|
|
126
|
+
service.onAuthStateChanged(state => reported.push(state));
|
|
127
|
+
expect(await service.waitForSignIn()).to.be.true;
|
|
209
128
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
setPassword: sinon.stub().resolves() as KeyStoreService['setPassword'],
|
|
214
|
-
getPassword: getPasswordStub as KeyStoreService['getPassword'],
|
|
215
|
-
deletePassword: deletePasswordStub as KeyStoreService['deletePassword'],
|
|
216
|
-
findPassword: sinon.stub().resolves(undefined) as KeyStoreService['findPassword'],
|
|
217
|
-
findCredentials: sinon.stub().resolves([]) as KeyStoreService['findCredentials'],
|
|
218
|
-
keys: sinon.stub().resolves([]) as KeyStoreService['keys']
|
|
219
|
-
};
|
|
129
|
+
expect(reported).to.deep.equal([{ isAuthenticated: true, accountLabel: 'octocat' }]);
|
|
130
|
+
expect(await service.getAuthState()).to.deep.equal({ isAuthenticated: true, accountLabel: 'octocat' });
|
|
131
|
+
});
|
|
220
132
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
133
|
+
it('should report the new state after signing out', async () => {
|
|
134
|
+
const service = new TestableCopilotAuthServiceImpl();
|
|
135
|
+
service.stubs.authState = { isAuthenticated: true, accountLabel: 'octocat' };
|
|
136
|
+
service.start();
|
|
137
|
+
await service.getAuthState();
|
|
224
138
|
|
|
225
|
-
|
|
226
|
-
|
|
139
|
+
const reported: CopilotAuthState[] = [];
|
|
140
|
+
service.onAuthStateChanged(state => reported.push(state));
|
|
141
|
+
await service.signOut();
|
|
227
142
|
|
|
228
|
-
|
|
229
|
-
|
|
143
|
+
expect(service.stubs.deleted).to.equal(1);
|
|
144
|
+
expect(reported).to.deep.equal([{ isAuthenticated: false }]);
|
|
230
145
|
});
|
|
146
|
+
});
|
|
231
147
|
|
|
232
|
-
|
|
233
|
-
getPasswordStub.resolves(JSON.stringify({
|
|
234
|
-
accessToken: 'gho_custom_token',
|
|
235
|
-
accountLabel: 'customuser'
|
|
236
|
-
}));
|
|
237
|
-
|
|
238
|
-
const token = await authService.getAccessToken();
|
|
148
|
+
describe('CopilotAuthServiceImpl - executable path', () => {
|
|
239
149
|
|
|
240
|
-
|
|
241
|
-
|
|
150
|
+
it('should hand the configured location to the lookup, which cannot read preferences itself', async () => {
|
|
151
|
+
const service = new TestableCopilotAuthServiceImpl();
|
|
152
|
+
service.start();
|
|
153
|
+
await service.setExecutablePath('/opt/copilot/copilot');
|
|
154
|
+
expect(service.stubs.configuredPath).to.equal('/opt/copilot/copilot');
|
|
242
155
|
});
|
|
243
156
|
|
|
244
|
-
it('should
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
157
|
+
it('should hand on that nothing is configured, so that the CLI is searched for', async () => {
|
|
158
|
+
const service = new TestableCopilotAuthServiceImpl();
|
|
159
|
+
service.start();
|
|
160
|
+
await service.setExecutablePath('/opt/copilot/copilot');
|
|
161
|
+
await service.setExecutablePath(undefined);
|
|
162
|
+
expect(service.stubs.configuredPath).to.be.undefined;
|
|
248
163
|
});
|
|
249
164
|
});
|