Path: blob/main/extensions/copilot/src/platform/authentication/test/node/simulationTestCopilotTokenManager.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { BugIndicatingError } from '../../../../util/vs/base/common/errors';6import { Emitter, Event, Relay } from '../../../../util/vs/base/common/event';7import { safeStringify } from '../../../../util/vs/base/common/objects';8import { NullEnvService } from '../../../env/common/nullEnvService';9import { CopilotToken, createTestExtendedTokenInfo, ExtendedTokenInfo, TokenEnvelope } from '../../common/copilotToken';10import { ICopilotTokenManager, nowSeconds } from '../../common/copilotTokenManager';1112export class SimulationTestCopilotTokenManager implements ICopilotTokenManager {13_serviceBrand: undefined;14private _actual = SingletonSimulationTestCopilotTokenManager.getInstance();15onDidCopilotTokenRefresh = this._actual.onDidCopilotTokenRefresh;1617getCopilotToken(force?: boolean): Promise<CopilotToken> {18return this._actual.getCopilotToken();19}2021resetCopilotToken(httpError?: number): void {22// nothing23}24}2526class SimulationTestFixedCopilotTokenManager {27public readonly onDidCopilotTokenRefresh = Event.None;2829constructor(30private _completionsToken: string,31) { }3233async getCopilotToken(): Promise<CopilotToken> {34return new CopilotToken(createTestExtendedTokenInfo({ token: this._completionsToken, username: 'fixedTokenManager', copilot_plan: 'unknown' }));35}36}3738let fetchAlreadyGoing = false;3940class SimulationTestCopilotTokenManagerFromGitHubToken {4142private readonly _onDidCopilotTokenRefresh = new Emitter<void>();43public readonly onDidCopilotTokenRefresh = this._onDidCopilotTokenRefresh.event;4445private _cachedToken: Promise<CopilotToken> | undefined;4647constructor(48private readonly _githubToken: string,49) { }5051async getCopilotToken(): Promise<CopilotToken> {52if (!this._cachedToken) {53this._cachedToken = this.fetchCopilotTokenFromGitHubToken();54}55return this._cachedToken;56}5758/**59* Fetches a Copilot token from the GitHub token.60*/61private async fetchCopilotTokenFromGitHubToken(): Promise<CopilotToken> {6263if (fetchAlreadyGoing) {64throw new BugIndicatingError(`This fetch should only happen once!`);65}66fetchAlreadyGoing = true;6768let response: Response;69try {70response = await fetch(71`https://api.github.com/copilot_internal/v2/token`,72{73headers: {74Authorization: `token ${this._githubToken}`,75...NullEnvService.Instance.getEditorVersionHeaders(),76}77}78);79} catch (err: unknown) {80let errAsString: string;81if (err instanceof Error) {82errAsString = `${err.stack ? err.stack : err.message}\n${'cause' in err ? 'Cause:\n' + err['cause'] : ''}`;83} else {84errAsString = safeStringify(err);85}86throw new Error(`Failed to get copilot token: ${errAsString}`);87}8889const tokenInfo: undefined | TokenEnvelope = await response.json() as any;90if (!response.ok || response.status === 401 || response.status === 403 || !tokenInfo || !tokenInfo.token) {91throw new Error(`Failed to get copilot token: ${response.status} ${response.statusText}`);92}9394// some users have clocks adjusted ahead, expires_at will immediately be less than current clock time;95// adjust expires_at to the refresh time + a buffer to avoid expiring the token before the refresh can fire.96tokenInfo.expires_at = nowSeconds() + tokenInfo.refresh_in + 60; // extra buffer to allow refresh to happen successfully9798// extend the token envelope99const extendedInfo: ExtendedTokenInfo = {100...tokenInfo,101username: 'NullUser',102copilot_plan: 'unknown',103isVscodeTeamMember: false,104organization_login_list: [],105};106107setTimeout(() => {108// refresh the promise109fetchAlreadyGoing = false; // reset the spam prevention flag as longer runs will need to refresh the token110this._cachedToken = this.fetchCopilotTokenFromGitHubToken();111this._onDidCopilotTokenRefresh.fire();112}, tokenInfo.refresh_in * 1000);113114return new CopilotToken(extendedInfo);115}116}117118/**119* This is written without any dependencies on any services because it is instantiated once across all tests.120* We do this to avoid fetching the copilot token and spamming the GitHub API.121*/122class SingletonSimulationTestCopilotTokenManager {123124private static _instance: SingletonSimulationTestCopilotTokenManager | null = null;125public static getInstance(): SingletonSimulationTestCopilotTokenManager {126if (!this._instance) {127this._instance = new SingletonSimulationTestCopilotTokenManager();128}129return this._instance;130}131132private _actual: SimulationTestFixedCopilotTokenManager | SimulationTestCopilotTokenManagerFromGitHubToken | undefined = undefined;133private onDidCopilotTokenRefreshRelay: Relay<void> = new Relay();134onDidCopilotTokenRefresh: Event<void> = this.onDidCopilotTokenRefreshRelay.event;135136getCopilotToken(): Promise<CopilotToken> {137if (!this._actual) {138if (process.env.GITHUB_PAT) {139this._actual = new SimulationTestFixedCopilotTokenManager(process.env.GITHUB_PAT);140} else if (process.env.GITHUB_OAUTH_TOKEN) {141this._actual = new SimulationTestCopilotTokenManagerFromGitHubToken(process.env.GITHUB_OAUTH_TOKEN);142} else {143throw new Error('Must set either GITHUB_PAT or GITHUB_OAUTH_TOKEN environment variable.');144}145this.onDidCopilotTokenRefreshRelay.input = this._actual.onDidCopilotTokenRefresh;146}147148return this._actual.getCopilotToken();149}150}151152153