Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/authentication/test/node/simulationTestCopilotTokenManager.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { BugIndicatingError } from '../../../../util/vs/base/common/errors';
7
import { Emitter, Event, Relay } from '../../../../util/vs/base/common/event';
8
import { safeStringify } from '../../../../util/vs/base/common/objects';
9
import { NullEnvService } from '../../../env/common/nullEnvService';
10
import { CopilotToken, createTestExtendedTokenInfo, ExtendedTokenInfo, TokenEnvelope } from '../../common/copilotToken';
11
import { ICopilotTokenManager, nowSeconds } from '../../common/copilotTokenManager';
12
13
export class SimulationTestCopilotTokenManager implements ICopilotTokenManager {
14
_serviceBrand: undefined;
15
private _actual = SingletonSimulationTestCopilotTokenManager.getInstance();
16
onDidCopilotTokenRefresh = this._actual.onDidCopilotTokenRefresh;
17
18
getCopilotToken(force?: boolean): Promise<CopilotToken> {
19
return this._actual.getCopilotToken();
20
}
21
22
resetCopilotToken(httpError?: number): void {
23
// nothing
24
}
25
}
26
27
class SimulationTestFixedCopilotTokenManager {
28
public readonly onDidCopilotTokenRefresh = Event.None;
29
30
constructor(
31
private _completionsToken: string,
32
) { }
33
34
async getCopilotToken(): Promise<CopilotToken> {
35
return new CopilotToken(createTestExtendedTokenInfo({ token: this._completionsToken, username: 'fixedTokenManager', copilot_plan: 'unknown' }));
36
}
37
}
38
39
let fetchAlreadyGoing = false;
40
41
class SimulationTestCopilotTokenManagerFromGitHubToken {
42
43
private readonly _onDidCopilotTokenRefresh = new Emitter<void>();
44
public readonly onDidCopilotTokenRefresh = this._onDidCopilotTokenRefresh.event;
45
46
private _cachedToken: Promise<CopilotToken> | undefined;
47
48
constructor(
49
private readonly _githubToken: string,
50
) { }
51
52
async getCopilotToken(): Promise<CopilotToken> {
53
if (!this._cachedToken) {
54
this._cachedToken = this.fetchCopilotTokenFromGitHubToken();
55
}
56
return this._cachedToken;
57
}
58
59
/**
60
* Fetches a Copilot token from the GitHub token.
61
*/
62
private async fetchCopilotTokenFromGitHubToken(): Promise<CopilotToken> {
63
64
if (fetchAlreadyGoing) {
65
throw new BugIndicatingError(`This fetch should only happen once!`);
66
}
67
fetchAlreadyGoing = true;
68
69
let response: Response;
70
try {
71
response = await fetch(
72
`https://api.github.com/copilot_internal/v2/token`,
73
{
74
headers: {
75
Authorization: `token ${this._githubToken}`,
76
...NullEnvService.Instance.getEditorVersionHeaders(),
77
}
78
}
79
);
80
} catch (err: unknown) {
81
let errAsString: string;
82
if (err instanceof Error) {
83
errAsString = `${err.stack ? err.stack : err.message}\n${'cause' in err ? 'Cause:\n' + err['cause'] : ''}`;
84
} else {
85
errAsString = safeStringify(err);
86
}
87
throw new Error(`Failed to get copilot token: ${errAsString}`);
88
}
89
90
const tokenInfo: undefined | TokenEnvelope = await response.json() as any;
91
if (!response.ok || response.status === 401 || response.status === 403 || !tokenInfo || !tokenInfo.token) {
92
throw new Error(`Failed to get copilot token: ${response.status} ${response.statusText}`);
93
}
94
95
// some users have clocks adjusted ahead, expires_at will immediately be less than current clock time;
96
// adjust expires_at to the refresh time + a buffer to avoid expiring the token before the refresh can fire.
97
tokenInfo.expires_at = nowSeconds() + tokenInfo.refresh_in + 60; // extra buffer to allow refresh to happen successfully
98
99
// extend the token envelope
100
const extendedInfo: ExtendedTokenInfo = {
101
...tokenInfo,
102
username: 'NullUser',
103
copilot_plan: 'unknown',
104
isVscodeTeamMember: false,
105
organization_login_list: [],
106
};
107
108
setTimeout(() => {
109
// refresh the promise
110
fetchAlreadyGoing = false; // reset the spam prevention flag as longer runs will need to refresh the token
111
this._cachedToken = this.fetchCopilotTokenFromGitHubToken();
112
this._onDidCopilotTokenRefresh.fire();
113
}, tokenInfo.refresh_in * 1000);
114
115
return new CopilotToken(extendedInfo);
116
}
117
}
118
119
/**
120
* This is written without any dependencies on any services because it is instantiated once across all tests.
121
* We do this to avoid fetching the copilot token and spamming the GitHub API.
122
*/
123
class SingletonSimulationTestCopilotTokenManager {
124
125
private static _instance: SingletonSimulationTestCopilotTokenManager | null = null;
126
public static getInstance(): SingletonSimulationTestCopilotTokenManager {
127
if (!this._instance) {
128
this._instance = new SingletonSimulationTestCopilotTokenManager();
129
}
130
return this._instance;
131
}
132
133
private _actual: SimulationTestFixedCopilotTokenManager | SimulationTestCopilotTokenManagerFromGitHubToken | undefined = undefined;
134
private onDidCopilotTokenRefreshRelay: Relay<void> = new Relay();
135
onDidCopilotTokenRefresh: Event<void> = this.onDidCopilotTokenRefreshRelay.event;
136
137
getCopilotToken(): Promise<CopilotToken> {
138
if (!this._actual) {
139
if (process.env.GITHUB_PAT) {
140
this._actual = new SimulationTestFixedCopilotTokenManager(process.env.GITHUB_PAT);
141
} else if (process.env.GITHUB_OAUTH_TOKEN) {
142
this._actual = new SimulationTestCopilotTokenManagerFromGitHubToken(process.env.GITHUB_OAUTH_TOKEN);
143
} else {
144
throw new Error('Must set either GITHUB_PAT or GITHUB_OAUTH_TOKEN environment variable.');
145
}
146
this.onDidCopilotTokenRefreshRelay.input = this._actual.onDidCopilotTokenRefresh;
147
}
148
149
return this._actual.getCopilotToken();
150
}
151
}
152
153