mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Merge from vscode 718331d6f3ebd1b571530ab499edb266ddd493d5
This commit is contained in:
@@ -17,14 +17,13 @@
|
||||
"main": "./out/extension.js",
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./"
|
||||
"compile": "gulp compile-extension:vscode-account",
|
||||
"watch": "gulp watch-extension:vscode-account"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^3.7.4",
|
||||
"tslint": "^5.12.1",
|
||||
"@types/node": "^10.12.21",
|
||||
"@types/keytar": "^4.0.1",
|
||||
"@types/vscode": "^1.41.0"
|
||||
"@types/keytar": "^4.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,12 @@ const clientId = 'aebc6443-996d-45c2-90f0-388ff96faa56';
|
||||
const tenant = 'organizations';
|
||||
|
||||
interface IToken {
|
||||
expiresIn: string; // How long access token is valid, in seconds
|
||||
accessToken: string;
|
||||
accessToken?: string; // When unable to refresh due to network problems, the access token becomes undefined
|
||||
|
||||
expiresIn?: string; // How long access token is valid, in seconds
|
||||
refreshToken: string;
|
||||
|
||||
displayName: string;
|
||||
accountName: string;
|
||||
scope: string;
|
||||
sessionId: string; // The account id + the scope
|
||||
}
|
||||
@@ -33,6 +34,7 @@ interface ITokenClaims {
|
||||
unique_name?: string;
|
||||
oid?: string;
|
||||
altsecid?: string;
|
||||
ipd?: string;
|
||||
scp: string;
|
||||
}
|
||||
|
||||
@@ -40,13 +42,36 @@ interface IStoredSession {
|
||||
id: string;
|
||||
refreshToken: string;
|
||||
scope: string; // Scopes are alphabetized and joined with a space
|
||||
accountName: string;
|
||||
}
|
||||
|
||||
function parseQuery(uri: vscode.Uri) {
|
||||
return uri.query.split('&').reduce((prev: any, current) => {
|
||||
const queryString = current.split('=');
|
||||
prev[queryString[0]] = queryString[1];
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export const onDidChangeSessions = new vscode.EventEmitter<void>();
|
||||
|
||||
export const REFRESH_NETWORK_FAILURE = 'Network failure';
|
||||
|
||||
class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
|
||||
public handleUri(uri: vscode.Uri) {
|
||||
this.fire(uri);
|
||||
}
|
||||
}
|
||||
|
||||
export class AzureActiveDirectoryService {
|
||||
private _tokens: IToken[] = [];
|
||||
private _refreshTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>();
|
||||
private _uriHandler: UriEventHandler;
|
||||
|
||||
constructor() {
|
||||
this._uriHandler = new UriEventHandler();
|
||||
vscode.window.registerUriHandler(this._uriHandler);
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
const storedData = await keychain.getToken();
|
||||
@@ -57,7 +82,21 @@ export class AzureActiveDirectoryService {
|
||||
try {
|
||||
await this.refreshToken(session.refreshToken, session.scope);
|
||||
} catch (e) {
|
||||
await this.logout(session.id);
|
||||
if (e.message === REFRESH_NETWORK_FAILURE) {
|
||||
const didSucceedOnRetry = await this.handleRefreshNetworkError(session.id, session.refreshToken, session.scope);
|
||||
if (!didSucceedOnRetry) {
|
||||
this._tokens.push({
|
||||
accessToken: undefined,
|
||||
refreshToken: session.refreshToken,
|
||||
accountName: session.accountName,
|
||||
scope: session.scope,
|
||||
sessionId: session.id
|
||||
});
|
||||
this.pollForReconnect(session.id, session.refreshToken, session.scope);
|
||||
}
|
||||
} else {
|
||||
await this.logout(session.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -79,7 +118,8 @@ export class AzureActiveDirectoryService {
|
||||
return {
|
||||
id: token.sessionId,
|
||||
refreshToken: token.refreshToken,
|
||||
scope: token.scope
|
||||
scope: token.scope,
|
||||
accountName: token.accountName
|
||||
};
|
||||
});
|
||||
|
||||
@@ -100,7 +140,11 @@ export class AzureActiveDirectoryService {
|
||||
await this.refreshToken(session.refreshToken, session.scope);
|
||||
didChange = true;
|
||||
} catch (e) {
|
||||
await this.logout(session.id);
|
||||
if (e.message === REFRESH_NETWORK_FAILURE) {
|
||||
// Ignore, will automatically retry on next poll.
|
||||
} else {
|
||||
await this.logout(session.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -136,11 +180,11 @@ export class AzureActiveDirectoryService {
|
||||
}, 1000 * 30);
|
||||
}
|
||||
|
||||
private convertToSession(token: IToken): vscode.Session {
|
||||
private convertToSession(token: IToken): vscode.AuthenticationSession {
|
||||
return {
|
||||
id: token.sessionId,
|
||||
accessToken: token.accessToken,
|
||||
displayName: token.displayName,
|
||||
accessToken: () => !token.accessToken ? Promise.reject('Unavailable due to network problems') : Promise.resolve(token.accessToken),
|
||||
accountName: token.accountName,
|
||||
scopes: token.scope.split(' ')
|
||||
};
|
||||
}
|
||||
@@ -154,12 +198,18 @@ export class AzureActiveDirectoryService {
|
||||
}
|
||||
}
|
||||
|
||||
get sessions(): vscode.Session[] {
|
||||
get sessions(): vscode.AuthenticationSession[] {
|
||||
return this._tokens.map(token => this.convertToSession(token));
|
||||
}
|
||||
|
||||
public async login(scope: string): Promise<void> {
|
||||
Logger.info('Logging in...');
|
||||
|
||||
if (vscode.env.uiKind === vscode.UIKind.Web) {
|
||||
await this.loginWithoutLocalServer(scope);
|
||||
return;
|
||||
}
|
||||
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
const { server, redirectPromise, codePromise } = createServer(nonce);
|
||||
|
||||
@@ -206,6 +256,13 @@ export class AzureActiveDirectoryService {
|
||||
res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` });
|
||||
res.end();
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error(e.message);
|
||||
|
||||
// If the error was about starting the server, try directly hitting the login endpoint instead
|
||||
if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') {
|
||||
await this.loginWithoutLocalServer(scope);
|
||||
}
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
server.close();
|
||||
@@ -213,28 +270,101 @@ export class AzureActiveDirectoryService {
|
||||
}
|
||||
}
|
||||
|
||||
private getCallbackEnvironment(callbackUri: vscode.Uri): string {
|
||||
switch (callbackUri.authority) {
|
||||
case 'online.visualstudio.com,':
|
||||
return 'vso';
|
||||
case 'online-ppe.core.vsengsaas.visualstudio.com':
|
||||
return 'vsoppe,';
|
||||
case 'online.dev.core.vsengsaas.visualstudio.com':
|
||||
return 'vsodev,';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private async loginWithoutLocalServer(scope: string): Promise<IToken> {
|
||||
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.vscode-account`));
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
|
||||
const callbackEnvironment = this.getCallbackEnvironment(callbackUri);
|
||||
const state = `${callbackEnvironment}${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`;
|
||||
const signInUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize`;
|
||||
let uri = vscode.Uri.parse(signInUrl);
|
||||
const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
|
||||
const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64'));
|
||||
uri = uri.with({
|
||||
query: `response_type=code&client_id=${encodeURIComponent(clientId)}&response_mode=query&redirect_uri=${redirectUrl}&state=${state}&scope=${scope}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`
|
||||
});
|
||||
vscode.env.openExternal(uri);
|
||||
|
||||
const timeoutPromise = new Promise((_: (value: IToken) => void, reject) => {
|
||||
const wait = setTimeout(() => {
|
||||
clearTimeout(wait);
|
||||
reject('Login timed out.');
|
||||
}, 1000 * 60 * 5);
|
||||
});
|
||||
|
||||
return Promise.race([this.handleCodeResponse(state, codeVerifier, scope), timeoutPromise]);
|
||||
}
|
||||
|
||||
private async handleCodeResponse(state: string, codeVerifier: string, scope: string) {
|
||||
let uriEventListener: vscode.Disposable;
|
||||
return new Promise((resolve: (value: IToken) => void, reject) => {
|
||||
uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => {
|
||||
try {
|
||||
const query = parseQuery(uri);
|
||||
const code = query.code;
|
||||
|
||||
if (query.state !== state) {
|
||||
throw new Error('State does not match.');
|
||||
}
|
||||
|
||||
const token = await this.exchangeCodeForToken(code, codeVerifier, scope);
|
||||
this.setToken(token, scope);
|
||||
|
||||
resolve(token);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}).then(result => {
|
||||
uriEventListener.dispose();
|
||||
return result;
|
||||
}).catch(err => {
|
||||
uriEventListener.dispose();
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
private async setToken(token: IToken, scope: string): Promise<void> {
|
||||
const existingToken = this._tokens.findIndex(t => t.sessionId === token.sessionId);
|
||||
if (existingToken) {
|
||||
this._tokens.splice(existingToken, 1, token);
|
||||
const existingTokenIndex = this._tokens.findIndex(t => t.sessionId === token.sessionId);
|
||||
if (existingTokenIndex > -1) {
|
||||
this._tokens.splice(existingTokenIndex, 1, token);
|
||||
} else {
|
||||
this._tokens.push(token);
|
||||
}
|
||||
|
||||
const existingTimeout = this._refreshTimeouts.get(token.sessionId);
|
||||
if (existingTimeout) {
|
||||
clearTimeout(existingTimeout);
|
||||
}
|
||||
this.clearSessionTimeout(token.sessionId);
|
||||
|
||||
this._refreshTimeouts.set(token.sessionId, setTimeout(async () => {
|
||||
try {
|
||||
await this.refreshToken(token.refreshToken, scope);
|
||||
} catch (e) {
|
||||
await this.logout(token.sessionId);
|
||||
} finally {
|
||||
onDidChangeSessions.fire();
|
||||
}
|
||||
}, 1000 * (parseInt(token.expiresIn) - 10)));
|
||||
if (token.expiresIn) {
|
||||
this._refreshTimeouts.set(token.sessionId, setTimeout(async () => {
|
||||
try {
|
||||
await this.refreshToken(token.refreshToken, scope);
|
||||
onDidChangeSessions.fire();
|
||||
} catch (e) {
|
||||
if (e.message === REFRESH_NETWORK_FAILURE) {
|
||||
const didSucceedOnRetry = await this.handleRefreshNetworkError(token.sessionId, token.refreshToken, scope);
|
||||
if (!didSucceedOnRetry) {
|
||||
this.pollForReconnect(token.sessionId, token.refreshToken, token.scope);
|
||||
}
|
||||
} else {
|
||||
await this.logout(token.sessionId);
|
||||
onDidChangeSessions.fire();
|
||||
}
|
||||
}
|
||||
}, 1000 * (parseInt(token.expiresIn) - 30)));
|
||||
}
|
||||
|
||||
this.storeTokenData();
|
||||
}
|
||||
@@ -247,8 +377,8 @@ export class AzureActiveDirectoryService {
|
||||
accessToken: json.access_token,
|
||||
refreshToken: json.refresh_token,
|
||||
scope,
|
||||
sessionId: claims.tid + (claims.oid || claims.altsecid) + scope,
|
||||
displayName: claims.email || claims.unique_name || 'user@example.com'
|
||||
sessionId: `${claims.tid}/${(claims.oid || (claims.altsecid || '' + claims.ipd || ''))}/${scope}`,
|
||||
accountName: claims.email || claims.unique_name || 'user@example.com'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -282,8 +412,10 @@ export class AzureActiveDirectoryService {
|
||||
});
|
||||
result.on('end', () => {
|
||||
if (result.statusCode === 200) {
|
||||
Logger.info('Exchanging login code for token success');
|
||||
resolve(this.getTokenFromResponse(buffer, scope));
|
||||
} else {
|
||||
Logger.error('Exchanging login code for token failed');
|
||||
reject(new Error('Unable to login.'));
|
||||
}
|
||||
});
|
||||
@@ -344,29 +476,80 @@ export class AzureActiveDirectoryService {
|
||||
post.end();
|
||||
post.on('error', err => {
|
||||
Logger.error(err.message);
|
||||
reject(err);
|
||||
reject(new Error(REFRESH_NETWORK_FAILURE));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async logout(sessionId: string) {
|
||||
Logger.info(`Logging out of session '${sessionId}'`);
|
||||
private clearSessionTimeout(sessionId: string): void {
|
||||
const timeout = this._refreshTimeouts.get(sessionId);
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
this._refreshTimeouts.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
private removeInMemorySessionData(sessionId: string) {
|
||||
const tokenIndex = this._tokens.findIndex(token => token.sessionId === sessionId);
|
||||
if (tokenIndex > -1) {
|
||||
this._tokens.splice(tokenIndex, 1);
|
||||
}
|
||||
|
||||
this.clearSessionTimeout(sessionId);
|
||||
}
|
||||
|
||||
private pollForReconnect(sessionId: string, refreshToken: string, scope: string): void {
|
||||
this.clearSessionTimeout(sessionId);
|
||||
|
||||
this._refreshTimeouts.set(sessionId, setTimeout(async () => {
|
||||
try {
|
||||
await this.refreshToken(refreshToken, scope);
|
||||
} catch (e) {
|
||||
this.pollForReconnect(sessionId, refreshToken, scope);
|
||||
}
|
||||
}, 1000 * 60 * 30));
|
||||
}
|
||||
|
||||
private handleRefreshNetworkError(sessionId: string, refreshToken: string, scope: string, attempts: number = 1): Promise<boolean> {
|
||||
return new Promise((resolve, _) => {
|
||||
if (attempts === 3) {
|
||||
Logger.error('Token refresh failed after 3 attempts');
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
if (attempts === 1) {
|
||||
const token = this._tokens.find(token => token.sessionId === sessionId);
|
||||
if (token) {
|
||||
token.accessToken = undefined;
|
||||
}
|
||||
|
||||
onDidChangeSessions.fire();
|
||||
}
|
||||
|
||||
const delayBeforeRetry = 5 * attempts * attempts;
|
||||
|
||||
this.clearSessionTimeout(sessionId);
|
||||
|
||||
this._refreshTimeouts.set(sessionId, setTimeout(async () => {
|
||||
try {
|
||||
await this.refreshToken(refreshToken, scope);
|
||||
return resolve(true);
|
||||
} catch (e) {
|
||||
return resolve(await this.handleRefreshNetworkError(sessionId, refreshToken, scope, attempts + 1));
|
||||
}
|
||||
}, 1000 * delayBeforeRetry));
|
||||
});
|
||||
}
|
||||
|
||||
public async logout(sessionId: string) {
|
||||
Logger.info(`Logging out of session '${sessionId}'`);
|
||||
this.removeInMemorySessionData(sessionId);
|
||||
|
||||
if (this._tokens.length === 0) {
|
||||
await keychain.deleteToken();
|
||||
} else {
|
||||
this.storeTokenData();
|
||||
}
|
||||
|
||||
const timeout = this._refreshTimeouts.get(sessionId);
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
this._refreshTimeouts.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
public async clearSessions() {
|
||||
|
||||
@@ -87,8 +87,8 @@ export async function startServer(server: http.Server): Promise<string> {
|
||||
}
|
||||
});
|
||||
|
||||
server.on('error', err => {
|
||||
reject(err);
|
||||
server.on('error', _ => {
|
||||
reject(new Error('Error listening to server'));
|
||||
});
|
||||
|
||||
server.on('close', () => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { AzureActiveDirectoryService, onDidChangeSessions } from './AADHelper';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
export async function activate(_: vscode.ExtensionContext) {
|
||||
|
||||
const loginService = new AzureActiveDirectoryService();
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// how we load it
|
||||
import * as keytarType from 'keytar';
|
||||
import { env } from 'vscode';
|
||||
import Logger from './logger';
|
||||
|
||||
function getKeytar(): Keytar | undefined {
|
||||
try {
|
||||
@@ -44,22 +45,27 @@ export class Keychain {
|
||||
return await this.keytar.setPassword(SERVICE_ID, ACCOUNT_ID, token);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
Logger.error(`Setting token failed: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getToken() {
|
||||
async getToken(): Promise<string | null | undefined> {
|
||||
try {
|
||||
return await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
Logger.error(`Getting token failed: ${e}`);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteToken() {
|
||||
async deleteToken(): Promise<boolean | undefined> {
|
||||
try {
|
||||
return await this.keytar.deletePassword(SERVICE_ID, ACCOUNT_ID);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
Logger.error(`Deleting token failed: ${e}`);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
extensions/vscode-account/src/typings/refs.d.ts
vendored
Normal file
7
extensions/vscode-account/src/typings/refs.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
@@ -1,61 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* This is the place for API experiments and proposals.
|
||||
* These API are NOT stable and subject to change. They are only available in the Insiders
|
||||
* distribution and CANNOT be used in published extensions.
|
||||
*
|
||||
* To test these API in local environment:
|
||||
* - Use Insiders release of VS Code.
|
||||
* - Add `"enableProposedApi": true` to your package.json.
|
||||
* - Copy this file to your project.
|
||||
*/
|
||||
|
||||
declare module 'vscode' {
|
||||
|
||||
export interface Session {
|
||||
id: string;
|
||||
accessToken: string;
|
||||
displayName: string;
|
||||
scopes: string[]
|
||||
}
|
||||
|
||||
export interface AuthenticationProvider {
|
||||
readonly id: string;
|
||||
readonly displayName: string;
|
||||
readonly onDidChangeSessions: Event<void>;
|
||||
|
||||
/**
|
||||
* Returns an array of current sessions.
|
||||
*/
|
||||
getSessions(): Promise<ReadonlyArray<Session>>;
|
||||
|
||||
/**
|
||||
* Prompts a user to login.
|
||||
*/
|
||||
login(scopes: string[]): Promise<Session>;
|
||||
logout(sessionId: string): Promise<void>;
|
||||
}
|
||||
|
||||
export namespace authentication {
|
||||
export function registerAuthenticationProvider(provider: AuthenticationProvider): Disposable;
|
||||
|
||||
/**
|
||||
* Fires with the provider id that was registered or unregistered.
|
||||
*/
|
||||
export const onDidRegisterAuthenticationProvider: Event<string>;
|
||||
export const onDidUnregisterAuthenticationProvider: Event<string>;
|
||||
|
||||
export const providers: ReadonlyArray<AuthenticationProvider>;
|
||||
}
|
||||
|
||||
// #region Ben - extension auth flow (desktop+web)
|
||||
|
||||
export namespace env {
|
||||
|
||||
export function asExternalUri(target: Uri): Thenable<Uri>
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,13 @@
|
||||
{
|
||||
"extends": "../shared.tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"es6",
|
||||
"es2016",
|
||||
"dom"
|
||||
],
|
||||
"outDir": "./out",
|
||||
"experimentalDecorators": true,
|
||||
"typeRoots": [
|
||||
"node_modules/@types",
|
||||
"src/typings"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"noImplicitAny": true
|
||||
"./node_modules/@types"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".vscode-test"
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -30,11 +30,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c"
|
||||
integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==
|
||||
|
||||
"@types/vscode@^1.41.0":
|
||||
version "1.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.41.0.tgz#b0d75920220f84e07093285e59180c0f11d336cd"
|
||||
integrity sha512-7SfeY5u9jgiELwxyLB3z7l6l/GbN9CqpCQGkcRlB7tKRFBxzbz2PoBfGrLxI1vRfUCIq5+hg5vtDHExwq5j3+A==
|
||||
|
||||
ansi-regex@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
||||
|
||||
Reference in New Issue
Block a user