fixed: 移除github,github-authentication,microsoft-authentication,tunnel-forwarding插件.

This commit is contained in:
chriswang521 2024-06-21 10:59:09 +08:00 committed by wangpenglong
parent e7570ec497
commit bf764842f5
121 changed files with 0 additions and 8671 deletions

View File

@ -40,11 +40,6 @@ const extensions = [
label: 'configuration-editing',
workspaceFolder: path.join(os.tmpdir(), `confeditout-${Math.floor(Math.random() * 100000)}`),
mocha: { timeout: 60_000 }
},
{
label: 'github-authentication',
workspaceFolder: path.join(os.tmpdir(), `msft-auth-${Math.floor(Math.random() * 100000)}`),
mocha: { timeout: 60_000 }
}
];

View File

@ -41,8 +41,6 @@ const compilations = [
'extension-editing/tsconfig.json',
'git/tsconfig.json',
'git-base/tsconfig.json',
'github-authentication/tsconfig.json',
'github/tsconfig.json',
'grunt/tsconfig.json',
'gulp/tsconfig.json',
'html-language-features/client/tsconfig.json',
@ -57,14 +55,12 @@ const compilations = [
'markdown-math/tsconfig.json',
'media-preview/tsconfig.json',
'merge-conflict/tsconfig.json',
'microsoft-authentication/tsconfig.json',
'notebook-renderers/tsconfig.json',
'npm/tsconfig.json',
'php-language-features/tsconfig.json',
'search-result/tsconfig.json',
'references-view/tsconfig.json',
'simple-browser/tsconfig.json',
'tunnel-forwarding/tsconfig.json',
'typescript-language-features/test-workspace/tsconfig.json',
'typescript-language-features/web/tsconfig.json',
'typescript-language-features/tsconfig.json',

View File

@ -320,8 +320,6 @@ const skippedExportMangledProjects = [
// These projects use webpack to dynamically rewrite imports, which messes up our mangling
'configuration-editing',
'microsoft-authentication',
'github-authentication',
'html-language-features/server',
];

View File

@ -19,8 +19,6 @@ const dirs = [
'extensions/extension-editing',
'extensions/git',
'extensions/git-base',
'extensions/github',
'extensions/github-authentication',
'extensions/grunt',
'extensions/gulp',
'extensions/html-language-features',
@ -34,14 +32,12 @@ const dirs = [
'extensions/markdown-math',
'extensions/media-preview',
'extensions/merge-conflict',
'extensions/microsoft-authentication',
'extensions/notebook-renderers',
'extensions/npm',
'extensions/php-language-features',
'extensions/references-view',
'extensions/search-result',
'extensions/simple-browser',
'extensions/tunnel-forwarding',
'extensions/typescript-language-features',
'extensions/vscode-api-tests',
'extensions/vscode-colorize-tests',

View File

@ -1 +0,0 @@
src/common/config.json

View File

@ -1,9 +0,0 @@
.gitignore
src/**
!src/common/config.json
out/**
build/**
extension.webpack.config.js
extension-browser.webpack.config.js
tsconfig.json
yarn.lock

View File

@ -1,7 +0,0 @@
# GitHub Authentication for Visual Studio Code
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
## Features
This extension provides support for authenticating to GitHub. It registers the `github` Authentication Provider that can be leveraged by other extensions. This also provides the GitHub authentication used by Settings Sync.

View File

@ -1,28 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const path = require('path');
const withBrowserDefaults = require('../shared.webpack.config').browser;
module.exports = withBrowserDefaults({
context: __dirname,
node: false,
entry: {
extension: './src/extension.ts',
},
resolve: {
alias: {
'uuid': path.resolve(__dirname, 'node_modules/uuid/dist/esm-browser/index.js'),
'./node/authServer': path.resolve(__dirname, 'src/browser/authServer'),
'./node/crypto': path.resolve(__dirname, 'src/browser/crypto'),
'./node/fetch': path.resolve(__dirname, 'src/browser/fetch'),
'./node/buffer': path.resolve(__dirname, 'src/browser/buffer'),
}
}
});

View File

@ -1,17 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withDefaults = require('../shared.webpack.config');
module.exports = withDefaults({
context: __dirname,
entry: {
extension: './src/extension.ts',
}
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1,37 +0,0 @@
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>GitHub Authentication - Sign In</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="auth.css" />
</head>
<body>
<a class="branding" href="https://code.visualstudio.com/">
Visual Studio Code
</a>
<div class="message-container">
<div class="message">
You are signed in now and can close this page.
</div>
<div class="error-message">
An error occurred while signing in:
<div class="error-text"></div>
</div>
</div>
<script>
var search = window.location.search;
var error = (/[?&^]error=([^&]+)/.exec(search) || [])[1];
if (error) {
document.querySelector('.error-text')
.textContent = decodeURIComponent(error);
document.querySelector('body')
.classList.add('error');
}
</script>
</body>
</html>

View File

@ -1,75 +0,0 @@
{
"name": "github-authentication",
"displayName": "%displayName%",
"description": "%description%",
"publisher": "vscode",
"license": "MIT",
"version": "0.0.2",
"engines": {
"vscode": "^1.41.0"
},
"icon": "images/icon.png",
"categories": [
"Other"
],
"api": "none",
"extensionKind": [
"ui",
"workspace"
],
"activationEvents": [],
"capabilities": {
"virtualWorkspaces": true,
"untrustedWorkspaces": {
"supported": "limited",
"restrictedConfigurations": [
"github-enterprise.uri"
]
}
},
"contributes": {
"authentication": [
{
"label": "GitHub",
"id": "github"
},
{
"label": "GitHub Enterprise Server",
"id": "github-enterprise"
}
],
"configuration": {
"title": "GitHub Enterprise Server Authentication Provider",
"properties": {
"github-enterprise.uri": {
"type": "string",
"description": "GitHub Enterprise Server URI"
}
}
}
},
"aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
"main": "./out/extension.js",
"browser": "./dist/browser/extension.js",
"scripts": {
"compile": "gulp compile-extension:github-authentication",
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
"watch": "gulp watch-extension:github-authentication",
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose",
"vscode:prepublish": "npm run compile"
},
"dependencies": {
"node-fetch": "2.6.7",
"@vscode/extension-telemetry": "^0.9.0",
"vscode-tas-client": "^0.1.47"
},
"devDependencies": {
"@types/mocha": "^9.1.1",
"@types/node": "18.x",
"@types/node-fetch": "^2.5.7"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/vscode.git"
}
}

View File

@ -1,4 +0,0 @@
{
"displayName": "GitHub Authentication",
"description": "GitHub Authentication Provider"
}

View File

@ -1,12 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function startServer(_: any): any {
throw new Error('Not implemented');
}
export function createServer(_: any): any {
throw new Error('Not implemented');
}

View File

@ -1,8 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function base64Encode(text: string): string {
return btoa(text);
}

View File

@ -1,6 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const crypto = globalThis.crypto;

View File

@ -1,6 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const fetching = fetch;

View File

@ -1,39 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri } from 'vscode';
import { AuthProviderType } from '../github';
const VALID_DESKTOP_CALLBACK_SCHEMES = [
'vscode',
'kylin-code',
'vscode-insiders',
// On Windows, some browsers don't seem to redirect back to OSS properly.
// As a result, you get stuck in the auth flow. We exclude this from the
// list until we can figure out a way to fix this behavior in browsers.
// 'code-oss',
'vscode-wsl',
'vscode-exploration'
];
export function isSupportedClient(uri: Uri): boolean {
return (
VALID_DESKTOP_CALLBACK_SCHEMES.includes(uri.scheme) ||
// vscode.dev & insiders.vscode.dev
/(?:^|\.)vscode\.dev$/.test(uri.authority) ||
// github.dev & codespaces
/(?:^|\.)github\.dev$/.test(uri.authority)
);
}
export function isSupportedTarget(type: AuthProviderType, gheUri?: Uri): boolean {
return (
type === AuthProviderType.github ||
isHostedGitHubEnterprise(gheUri!)
);
}
export function isHostedGitHubEnterprise(uri: Uri): boolean {
return /\.ghe\.com$/.test(uri.authority);
}

View File

@ -1,10 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const TIMED_OUT_ERROR = 'Timed out';
// These error messages are internal and should not be shown to the user in any way.
export const USER_CANCELLATION_ERROR = 'User Cancelled';
export const NETWORK_ERROR = 'network error';

View File

@ -1,97 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import { getExperimentationService, IExperimentationService, IExperimentationTelemetry, TargetPopulation } from 'vscode-tas-client';
export class ExperimentationTelemetry implements IExperimentationTelemetry {
private sharedProperties: Record<string, string> = {};
private experimentationServicePromise: Promise<IExperimentationService> | undefined;
constructor(private readonly context: vscode.ExtensionContext, private baseReporter: TelemetryReporter) { }
private async createExperimentationService(): Promise<IExperimentationService> {
let targetPopulation: TargetPopulation;
switch (vscode.env.uriScheme) {
case 'vscode':
case 'kylin-code':
targetPopulation = TargetPopulation.Public;
break;
case 'vscode-insiders':
targetPopulation = TargetPopulation.Insiders;
break;
case 'vscode-exploration':
targetPopulation = TargetPopulation.Internal;
break;
case 'code-oss':
targetPopulation = TargetPopulation.Team;
break;
default:
targetPopulation = TargetPopulation.Public;
break;
}
const id = this.context.extension.id;
const version = this.context.extension.packageJSON.version;
const experimentationService = getExperimentationService(id, version, targetPopulation, this, this.context.globalState);
await experimentationService.initialFetch;
return experimentationService;
}
/**
* @returns A promise that you shouldn't need to await because this is just telemetry.
*/
async sendTelemetryEvent(eventName: string, properties?: Record<string, string>, measurements?: Record<string, number>) {
if (!this.experimentationServicePromise) {
this.experimentationServicePromise = this.createExperimentationService();
}
await this.experimentationServicePromise;
this.baseReporter.sendTelemetryEvent(
eventName,
{
...this.sharedProperties,
...properties,
},
measurements,
);
}
/**
* @returns A promise that you shouldn't need to await because this is just telemetry.
*/
async sendTelemetryErrorEvent(
eventName: string,
properties?: Record<string, string>,
_measurements?: Record<string, number>
) {
if (!this.experimentationServicePromise) {
this.experimentationServicePromise = this.createExperimentationService();
}
await this.experimentationServicePromise;
this.baseReporter.sendTelemetryErrorEvent(eventName, {
...this.sharedProperties,
...properties,
});
}
setSharedProperty(name: string, value: string): void {
this.sharedProperties[name] = value;
}
postEvent(eventName: string, props: Map<string, string>): void {
const event: Record<string, string> = {};
for (const [key, value] of props) {
event[key] = value;
}
this.sendTelemetryEvent(eventName, event);
}
dispose(): Promise<any> {
return this.baseReporter.dispose();
}
}

View File

@ -1,48 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { Log } from './logger';
export class Keychain {
constructor(
private readonly context: vscode.ExtensionContext,
private readonly serviceId: string,
private readonly Logger: Log
) { }
async setToken(token: string): Promise<void> {
try {
return await this.context.secrets.store(this.serviceId, token);
} catch (e) {
// Ignore
this.Logger.error(`Setting token failed: ${e}`);
}
}
async getToken(): Promise<string | null | undefined> {
try {
const secret = await this.context.secrets.get(this.serviceId);
if (secret && secret !== '[]') {
this.Logger.trace('Token acquired from secret storage.');
}
return secret;
} catch (e) {
// Ignore
this.Logger.error(`Getting token failed: ${e}`);
return Promise.resolve(undefined);
}
}
async deleteToken(): Promise<void> {
try {
return await this.context.secrets.delete(this.serviceId);
} catch (e) {
// Ignore
this.Logger.error(`Deleting token failed: ${e}`);
return Promise.resolve(undefined);
}
}
}

View File

@ -1,32 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { AuthProviderType } from '../github';
export class Log {
private output: vscode.LogOutputChannel;
constructor(private readonly type: AuthProviderType) {
const friendlyName = this.type === AuthProviderType.github ? 'GitHub' : 'GitHub Enterprise';
this.output = vscode.window.createOutputChannel(`${friendlyName} Authentication`, { log: true });
}
public trace(message: string): void {
this.output.trace(message);
}
public info(message: string): void {
this.output.info(message);
}
public error(message: string): void {
this.output.error(message);
}
public warn(message: string): void {
this.output.warn(message);
}
}

View File

@ -1,118 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EventEmitter, Event, Disposable } from 'vscode';
export function filterEvent<T>(event: Event<T>, filter: (e: T) => boolean): Event<T> {
return (listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
}
export function onceEvent<T>(event: Event<T>): Event<T> {
return (listener, thisArgs = null, disposables?) => {
const result = event(e => {
result.dispose();
return listener.call(thisArgs, e);
}, null, disposables);
return result;
};
}
export interface PromiseAdapter<T, U> {
(
value: T,
resolve:
(value: U | PromiseLike<U>) => void,
reject:
(reason: any) => void
): any;
}
const passthrough = (value: any, resolve: (value?: any) => void) => resolve(value);
/**
* Return a promise that resolves with the next emitted event, or with some future
* event as decided by an adapter.
*
* If specified, the adapter is a function that will be called with
* `(event, resolve, reject)`. It will be called once per event until it resolves or
* rejects.
*
* The default adapter is the passthrough function `(value, resolve) => resolve(value)`.
*
* @param event the event
* @param adapter controls resolution of the returned promise
* @returns a promise that resolves or rejects as specified by the adapter
*/
export function promiseFromEvent<T, U>(
event: Event<T>,
adapter: PromiseAdapter<T, U> = passthrough): { promise: Promise<U>; cancel: EventEmitter<void> } {
let subscription: Disposable;
const cancel = new EventEmitter<void>();
return {
promise: new Promise<U>((resolve, reject) => {
cancel.event(_ => reject('Cancelled'));
subscription = event((value: T) => {
try {
Promise.resolve(adapter(value, resolve, reject))
.catch(reject);
} catch (error) {
reject(error);
}
});
}).then(
(result: U) => {
subscription.dispose();
return result;
},
error => {
subscription.dispose();
throw error;
}
),
cancel
};
}
export function arrayEquals<T>(one: ReadonlyArray<T> | undefined, other: ReadonlyArray<T> | undefined, itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean {
if (one === other) {
return true;
}
if (!one || !other) {
return false;
}
if (one.length !== other.length) {
return false;
}
for (let i = 0, len = one.length; i < len; i++) {
if (!itemEquals(one[i], other[i])) {
return false;
}
}
return true;
}
export class StopWatch {
private _startTime: number = Date.now();
private _stopTime: number = -1;
public stop(): void {
this._stopTime = Date.now();
}
public elapsed(): number {
if (this._stopTime !== -1) {
return this._stopTime - this._startTime;
}
return Date.now() - this._startTime;
}
}

View File

@ -1,15 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface IConfig {
// The client ID of the GitHub OAuth app
gitHubClientId: string;
gitHubClientSecret?: string;
}
// For easy access to mixin client ID and secret
export const Config: IConfig = {
gitHubClientId: '01ab8ac9400c4e429b23'
};

View File

@ -1,46 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { GitHubAuthenticationProvider, UriEventHandler } from './github';
function initGHES(context: vscode.ExtensionContext, uriHandler: UriEventHandler) {
const settingValue = vscode.workspace.getConfiguration().get<string>('github-enterprise.uri');
if (!settingValue) {
return undefined;
}
// validate user value
let uri: vscode.Uri;
try {
uri = vscode.Uri.parse(settingValue, true);
} catch (e) {
vscode.window.showErrorMessage(vscode.l10n.t('GitHub Enterprise Server URI is not a valid URI: {0}', e.message ?? e));
return;
}
const githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, uriHandler, uri);
context.subscriptions.push(githubEnterpriseAuthProvider);
return githubEnterpriseAuthProvider;
}
export function activate(context: vscode.ExtensionContext) {
const uriHandler = new UriEventHandler();
context.subscriptions.push(uriHandler);
context.subscriptions.push(vscode.window.registerUriHandler(uriHandler));
context.subscriptions.push(new GitHubAuthenticationProvider(context, uriHandler));
let githubEnterpriseAuthProvider: GitHubAuthenticationProvider | undefined = initGHES(context, uriHandler);
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration('github-enterprise.uri')) {
if (vscode.workspace.getConfiguration().get<string>('github-enterprise.uri')) {
githubEnterpriseAuthProvider?.dispose();
githubEnterpriseAuthProvider = initGHES(context, uriHandler);
}
}
}));
}

View File

@ -1,502 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { ProgressLocation, Uri, commands, env, l10n, window } from 'vscode';
import { Log } from './common/logger';
import { Config } from './config';
import { UriEventHandler } from './github';
import { fetching } from './node/fetch';
import { LoopbackAuthServer } from './node/authServer';
import { promiseFromEvent } from './common/utils';
import { isHostedGitHubEnterprise } from './common/env';
import { NETWORK_ERROR, TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors';
interface IGitHubDeviceCodeResponse {
device_code: string;
user_code: string;
verification_uri: string;
interval: number;
}
interface IFlowOptions {
// GitHub.com
readonly supportsGitHubDotCom: boolean;
// A GitHub Enterprise Server that is hosted by an organization
readonly supportsGitHubEnterpriseServer: boolean;
// A GitHub Enterprise Server that is hosted by GitHub for an organization
readonly supportsHostedGitHubEnterprise: boolean;
// Runtimes - there are constraints on which runtimes support which flows
readonly supportsWebWorkerExtensionHost: boolean;
readonly supportsRemoteExtensionHost: boolean;
// Clients - see `isSupportedClient` in `common/env.ts` for what constitutes a supported client
readonly supportsSupportedClients: boolean;
readonly supportsUnsupportedClients: boolean;
// Configurations - some flows require a client secret
readonly supportsNoClientSecret: boolean;
}
export const enum GitHubTarget {
DotCom,
Enterprise,
HostedEnterprise
}
export const enum ExtensionHost {
WebWorker,
Remote,
Local
}
export interface IFlowQuery {
target: GitHubTarget;
extensionHost: ExtensionHost;
isSupportedClient: boolean;
}
interface IFlowTriggerOptions {
scopes: string;
baseUri: Uri;
logger: Log;
redirectUri: Uri;
nonce: string;
callbackUri: Uri;
uriHandler: UriEventHandler;
enterpriseUri?: Uri;
}
interface IFlow {
label: string;
options: IFlowOptions;
trigger(options: IFlowTriggerOptions): Promise<string>;
}
async function exchangeCodeForToken(
logger: Log,
endpointUri: Uri,
redirectUri: Uri,
code: string,
enterpriseUri?: Uri
): Promise<string> {
logger.info('Exchanging code for token...');
const clientSecret = Config.gitHubClientSecret;
if (!clientSecret) {
throw new Error('No client secret configured for GitHub authentication.');
}
const body = new URLSearchParams([
['code', code],
['client_id', Config.gitHubClientId],
['redirect_uri', redirectUri.toString(true)],
['client_secret', clientSecret]
]);
if (enterpriseUri) {
body.append('github_enterprise', enterpriseUri.toString(true));
}
const result = await fetching(endpointUri.toString(true), {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': body.toString()
},
body: body.toString()
});
if (result.ok) {
const json = await result.json();
logger.info('Token exchange success!');
return json.access_token;
} else {
const text = await result.text();
const error = new Error(text);
error.name = 'GitHubTokenExchangeError';
throw error;
}
}
const allFlows: IFlow[] = [
new class UrlHandlerFlow implements IFlow {
label = l10n.t('url handler');
options: IFlowOptions = {
supportsGitHubDotCom: true,
// Supporting GHES would be challenging because different versions
// used a different client ID. We could try to detect the version
// and use the right one, but that's a lot of work when we have
// other flows that work well.
supportsGitHubEnterpriseServer: false,
supportsHostedGitHubEnterprise: true,
supportsRemoteExtensionHost: true,
supportsWebWorkerExtensionHost: true,
// exchanging a code for a token requires a client secret
supportsNoClientSecret: false,
supportsSupportedClients: true,
supportsUnsupportedClients: false
};
async trigger({
scopes,
baseUri,
redirectUri,
logger,
nonce,
callbackUri,
uriHandler,
enterpriseUri
}: IFlowTriggerOptions): Promise<string> {
logger.info(`Trying without local server... (${scopes})`);
return await window.withProgress<string>({
location: ProgressLocation.Notification,
title: l10n.t({
message: 'Signing in to {0}...',
args: [baseUri.authority],
comment: ['The {0} will be a url, e.g. github.com']
}),
cancellable: true
}, async (_, token) => {
const promise = uriHandler.waitForCode(logger, scopes, nonce, token);
const searchParams = new URLSearchParams([
['client_id', Config.gitHubClientId],
['redirect_uri', redirectUri.toString(true)],
['scope', scopes],
['state', encodeURIComponent(callbackUri.toString(true))]
]);
// The extra toString, parse is apparently needed for env.openExternal
// to open the correct URL.
const uri = Uri.parse(baseUri.with({
path: '/login/oauth/authorize',
query: searchParams.toString()
}).toString(true));
await env.openExternal(uri);
const code = await promise;
const proxyEndpoints: { [providerId: string]: string } | undefined = await commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
const endpointUrl = proxyEndpoints?.github
? Uri.parse(`${proxyEndpoints.github}login/oauth/access_token`)
: baseUri.with({ path: '/login/oauth/access_token' });
const accessToken = await exchangeCodeForToken(logger, endpointUrl, redirectUri, code, enterpriseUri);
return accessToken;
});
}
},
new class LocalServerFlow implements IFlow {
label = l10n.t('local server');
options: IFlowOptions = {
supportsGitHubDotCom: true,
// Supporting GHES would be challenging because different versions
// used a different client ID. We could try to detect the version
// and use the right one, but that's a lot of work when we have
// other flows that work well.
supportsGitHubEnterpriseServer: false,
supportsHostedGitHubEnterprise: true,
// Opening a port on the remote side can't be open in the browser on
// the client side so this flow won't work in remote extension hosts
supportsRemoteExtensionHost: false,
// Web worker can't open a port to listen for the redirect
supportsWebWorkerExtensionHost: false,
// exchanging a code for a token requires a client secret
supportsNoClientSecret: false,
supportsSupportedClients: true,
supportsUnsupportedClients: true
};
async trigger({
scopes,
baseUri,
redirectUri,
logger,
enterpriseUri
}: IFlowTriggerOptions): Promise<string> {
logger.info(`Trying with local server... (${scopes})`);
return await window.withProgress<string>({
location: ProgressLocation.Notification,
title: l10n.t({
message: 'Signing in to {0}...',
args: [baseUri.authority],
comment: ['The {0} will be a url, e.g. github.com']
}),
cancellable: true
}, async (_, token) => {
const searchParams = new URLSearchParams([
['client_id', Config.gitHubClientId],
['redirect_uri', redirectUri.toString(true)],
['scope', scopes],
]);
const loginUrl = baseUri.with({
path: '/login/oauth/authorize',
query: searchParams.toString()
});
const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl.toString(true));
const port = await server.start();
let codeToExchange;
try {
env.openExternal(Uri.parse(`http://127.0.0.1:${port}/signin?nonce=${encodeURIComponent(server.nonce)}`));
const { code } = await Promise.race([
server.waitForOAuthResponse(),
new Promise<any>((_, reject) => setTimeout(() => reject(TIMED_OUT_ERROR), 300_000)), // 5min timeout
promiseFromEvent<any, any>(token.onCancellationRequested, (_, __, reject) => { reject(USER_CANCELLATION_ERROR); }).promise
]);
codeToExchange = code;
} finally {
setTimeout(() => {
void server.stop();
}, 5000);
}
const accessToken = await exchangeCodeForToken(
logger,
baseUri.with({ path: '/login/oauth/access_token' }),
redirectUri,
codeToExchange,
enterpriseUri);
return accessToken;
});
}
},
new class DeviceCodeFlow implements IFlow {
label = l10n.t('device code');
options: IFlowOptions = {
supportsGitHubDotCom: true,
supportsGitHubEnterpriseServer: true,
supportsHostedGitHubEnterprise: true,
supportsRemoteExtensionHost: true,
// CORS prevents this from working in web workers
supportsWebWorkerExtensionHost: false,
supportsNoClientSecret: true,
supportsSupportedClients: true,
supportsUnsupportedClients: true
};
async trigger({ scopes, baseUri, logger }: IFlowTriggerOptions) {
logger.info(`Trying device code flow... (${scopes})`);
// Get initial device code
const uri = baseUri.with({
path: '/login/device/code',
query: `client_id=${Config.gitHubClientId}&scope=${scopes}`
});
const result = await fetching(uri.toString(true), {
method: 'POST',
headers: {
Accept: 'application/json'
}
});
if (!result.ok) {
throw new Error(`Failed to get one-time code: ${await result.text()}`);
}
const json = await result.json() as IGitHubDeviceCodeResponse;
const button = l10n.t('Copy & Continue to GitHub');
const modalResult = await window.showInformationMessage(
l10n.t({ message: 'Your Code: {0}', args: [json.user_code], comment: ['The {0} will be a code, e.g. 123-456'] }),
{
modal: true,
detail: l10n.t('To finish authenticating, navigate to GitHub and paste in the above one-time code.')
}, button);
if (modalResult !== button) {
throw new Error(USER_CANCELLATION_ERROR);
}
await env.clipboard.writeText(json.user_code);
const uriToOpen = await env.asExternalUri(Uri.parse(json.verification_uri));
await env.openExternal(uriToOpen);
return await this.waitForDeviceCodeAccessToken(baseUri, json);
}
private async waitForDeviceCodeAccessToken(
baseUri: Uri,
json: IGitHubDeviceCodeResponse,
): Promise<string> {
return await window.withProgress<string>({
location: ProgressLocation.Notification,
cancellable: true,
title: l10n.t({
message: 'Open [{0}]({0}) in a new tab and paste your one-time code: {1}',
args: [json.verification_uri, json.user_code],
comment: [
'The [{0}]({0}) will be a url and the {1} will be a code, e.g. 123-456',
'{Locked="[{0}]({0})"}'
]
})
}, async (_, token) => {
const refreshTokenUri = baseUri.with({
path: '/login/oauth/access_token',
query: `client_id=${Config.gitHubClientId}&device_code=${json.device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code`
});
// Try for 2 minutes
const attempts = 120 / json.interval;
for (let i = 0; i < attempts; i++) {
await new Promise(resolve => setTimeout(resolve, json.interval * 1000));
if (token.isCancellationRequested) {
throw new Error(USER_CANCELLATION_ERROR);
}
let accessTokenResult;
try {
accessTokenResult = await fetching(refreshTokenUri.toString(true), {
method: 'POST',
headers: {
Accept: 'application/json'
}
});
} catch {
continue;
}
if (!accessTokenResult.ok) {
continue;
}
const accessTokenJson = await accessTokenResult.json();
if (accessTokenJson.error === 'authorization_pending') {
continue;
}
if (accessTokenJson.error) {
throw new Error(accessTokenJson.error_description);
}
return accessTokenJson.access_token;
}
throw new Error(TIMED_OUT_ERROR);
});
}
},
new class PatFlow implements IFlow {
label = l10n.t('personal access token');
options: IFlowOptions = {
supportsGitHubDotCom: true,
supportsGitHubEnterpriseServer: true,
supportsHostedGitHubEnterprise: true,
supportsRemoteExtensionHost: true,
supportsWebWorkerExtensionHost: true,
supportsNoClientSecret: true,
// PATs can't be used with Settings Sync so we don't enable this flow
// for supported clients
supportsSupportedClients: false,
supportsUnsupportedClients: true
};
async trigger({ scopes, baseUri, logger, enterpriseUri }: IFlowTriggerOptions) {
logger.info(`Trying to retrieve PAT... (${scopes})`);
const button = l10n.t('Continue to GitHub');
const modalResult = await window.showInformationMessage(
l10n.t('Continue to GitHub to create a Personal Access Token (PAT)'),
{
modal: true,
detail: l10n.t('To finish authenticating, navigate to GitHub to create a PAT then paste the PAT into the input box.')
}, button);
if (modalResult !== button) {
throw new Error(USER_CANCELLATION_ERROR);
}
const description = `${env.appName} (${scopes})`;
const uriToOpen = await env.asExternalUri(baseUri.with({ path: '/settings/tokens/new', query: `description=${description}&scopes=${scopes.split(' ').join(',')}` }));
await env.openExternal(uriToOpen);
const token = await window.showInputBox({ placeHolder: `ghp_1a2b3c4...`, prompt: `GitHub Personal Access Token - ${scopes}`, ignoreFocusOut: true });
if (!token) { throw new Error(USER_CANCELLATION_ERROR); }
const appUri = !enterpriseUri || isHostedGitHubEnterprise(enterpriseUri)
? Uri.parse(`${baseUri.scheme}://api.${baseUri.authority}`)
: Uri.parse(`${baseUri.scheme}://${baseUri.authority}/api/v3`);
const tokenScopes = await this.getScopes(token, appUri, logger); // Example: ['repo', 'user']
const scopesList = scopes.split(' '); // Example: 'read:user repo user:email'
if (!scopesList.every(scope => {
const included = tokenScopes.includes(scope);
if (included || !scope.includes(':')) {
return included;
}
return scope.split(':').some(splitScopes => {
return tokenScopes.includes(splitScopes);
});
})) {
throw new Error(`The provided token does not match the requested scopes: ${scopes}`);
}
return token;
}
private async getScopes(token: string, serverUri: Uri, logger: Log): Promise<string[]> {
try {
logger.info('Getting token scopes...');
const result = await fetching(serverUri.toString(), {
headers: {
Authorization: `token ${token}`,
'User-Agent': `${env.appName} (${env.appHost})`
}
});
if (result.ok) {
const scopes = result.headers.get('X-OAuth-Scopes');
return scopes ? scopes.split(',').map(scope => scope.trim()) : [];
} else {
logger.error(`Getting scopes failed: ${result.statusText}`);
throw new Error(result.statusText);
}
} catch (ex) {
logger.error(ex.message);
throw new Error(NETWORK_ERROR);
}
}
}
];
export function getFlows(query: IFlowQuery) {
return allFlows.filter(flow => {
let useFlow: boolean = true;
switch (query.target) {
case GitHubTarget.DotCom:
useFlow &&= flow.options.supportsGitHubDotCom;
break;
case GitHubTarget.Enterprise:
useFlow &&= flow.options.supportsGitHubEnterpriseServer;
break;
case GitHubTarget.HostedEnterprise:
useFlow &&= flow.options.supportsHostedGitHubEnterprise;
break;
}
switch (query.extensionHost) {
case ExtensionHost.Remote:
useFlow &&= flow.options.supportsRemoteExtensionHost;
break;
case ExtensionHost.WebWorker:
useFlow &&= flow.options.supportsWebWorkerExtensionHost;
break;
}
if (!Config.gitHubClientSecret) {
useFlow &&= flow.options.supportsNoClientSecret;
}
if (query.isSupportedClient) {
// TODO: revisit how we support PAT in GHES but not DotCom... but this works for now since
// there isn't another flow that has supportsSupportedClients = false
useFlow &&= (flow.options.supportsSupportedClients || query.target !== GitHubTarget.DotCom);
} else {
useFlow &&= flow.options.supportsUnsupportedClients;
}
return useFlow;
});
}

View File

@ -1,383 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import { Keychain } from './common/keychain';
import { GitHubServer, IGitHubServer } from './githubServer';
import { PromiseAdapter, arrayEquals, promiseFromEvent } from './common/utils';
import { ExperimentationTelemetry } from './common/experimentationService';
import { Log } from './common/logger';
import { crypto } from './node/crypto';
import { TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors';
interface SessionData {
id: string;
account?: {
label?: string;
displayName?: string;
id: string;
};
scopes: string[];
accessToken: string;
}
export enum AuthProviderType {
github = 'github',
githubEnterprise = 'github-enterprise'
}
export class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
private readonly _pendingNonces = new Map<string, string[]>();
private readonly _codeExchangePromises = new Map<string, { promise: Promise<string>; cancel: vscode.EventEmitter<void> }>();
public handleUri(uri: vscode.Uri) {
this.fire(uri);
}
public async waitForCode(logger: Log, scopes: string, nonce: string, token: vscode.CancellationToken) {
const existingNonces = this._pendingNonces.get(scopes) || [];
this._pendingNonces.set(scopes, [...existingNonces, nonce]);
let codeExchangePromise = this._codeExchangePromises.get(scopes);
if (!codeExchangePromise) {
codeExchangePromise = promiseFromEvent(this.event, this.handleEvent(logger, scopes));
this._codeExchangePromises.set(scopes, codeExchangePromise);
}
try {
return await Promise.race([
codeExchangePromise.promise,
new Promise<string>((_, reject) => setTimeout(() => reject(TIMED_OUT_ERROR), 300_000)), // 5min timeout
promiseFromEvent<void, string>(token.onCancellationRequested, (_, __, reject) => { reject(USER_CANCELLATION_ERROR); }).promise
]);
} finally {
this._pendingNonces.delete(scopes);
codeExchangePromise?.cancel.fire();
this._codeExchangePromises.delete(scopes);
}
}
private handleEvent: (logger: Log, scopes: string) => PromiseAdapter<vscode.Uri, string> =
(logger: Log, scopes) => (uri, resolve, reject) => {
const query = new URLSearchParams(uri.query);
const code = query.get('code');
const nonce = query.get('nonce');
if (!code) {
reject(new Error('No code'));
return;
}
if (!nonce) {
reject(new Error('No nonce'));
return;
}
const acceptedNonces = this._pendingNonces.get(scopes) || [];
if (!acceptedNonces.includes(nonce)) {
// A common scenario of this happening is if you:
// 1. Trigger a sign in with one set of scopes
// 2. Before finishing 1, you trigger a sign in with a different set of scopes
// In this scenario we should just return and wait for the next UriHandler event
// to run as we are probably still waiting on the user to hit 'Continue'
logger.info('Nonce not found in accepted nonces. Skipping this execution...');
return;
}
resolve(code);
};
}
export class GitHubAuthenticationProvider implements vscode.AuthenticationProvider, vscode.Disposable {
private readonly _sessionChangeEmitter = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
private readonly _logger: Log;
private readonly _githubServer: IGitHubServer;
private readonly _telemetryReporter: ExperimentationTelemetry;
private readonly _keychain: Keychain;
private readonly _accountsSeen = new Set<string>();
private readonly _disposable: vscode.Disposable | undefined;
private _sessionsPromise: Promise<vscode.AuthenticationSession[]>;
constructor(
private readonly context: vscode.ExtensionContext,
uriHandler: UriEventHandler,
ghesUri?: vscode.Uri
) {
const { aiKey } = context.extension.packageJSON as { name: string; version: string; aiKey: string };
this._telemetryReporter = new ExperimentationTelemetry(context, new TelemetryReporter(aiKey));
const type = ghesUri ? AuthProviderType.githubEnterprise : AuthProviderType.github;
this._logger = new Log(type);
this._keychain = new Keychain(
this.context,
type === AuthProviderType.github
? `${type}.auth`
: `${ghesUri?.authority}${ghesUri?.path}.ghes.auth`,
this._logger);
this._githubServer = new GitHubServer(
this._logger,
this._telemetryReporter,
uriHandler,
context.extension.extensionKind,
ghesUri);
// Contains the current state of the sessions we have available.
this._sessionsPromise = this.readSessions().then((sessions) => {
// fire telemetry after a second to allow the workbench to focus on loading
setTimeout(() => sessions.forEach(s => this.afterSessionLoad(s)), 1000);
return sessions;
});
this._disposable = vscode.Disposable.from(
this._telemetryReporter,
vscode.authentication.registerAuthenticationProvider(type, this._githubServer.friendlyName, this, { supportsMultipleAccounts: false }),
this.context.secrets.onDidChange(() => this.checkForUpdates())
);
}
dispose() {
this._disposable?.dispose();
}
get onDidChangeSessions() {
return this._sessionChangeEmitter.event;
}
async getSessions(scopes?: string[]): Promise<vscode.AuthenticationSession[]> {
// For GitHub scope list, order doesn't matter so we immediately sort the scopes
const sortedScopes = scopes?.sort() || [];
this._logger.info(`Getting sessions for ${sortedScopes.length ? sortedScopes.join(',') : 'all scopes'}...`);
const sessions = await this._sessionsPromise;
const finalSessions = sortedScopes.length
? sessions.filter(session => arrayEquals([...session.scopes].sort(), sortedScopes))
: sessions;
this._logger.info(`Got ${finalSessions.length} sessions for ${sortedScopes?.join(',') ?? 'all scopes'}...`);
return finalSessions;
}
private async afterSessionLoad(session: vscode.AuthenticationSession): Promise<void> {
// We only want to fire a telemetry if we haven't seen this account yet in this session.
if (!this._accountsSeen.has(session.account.id)) {
this._accountsSeen.add(session.account.id);
this._githubServer.sendAdditionalTelemetryInfo(session);
}
}
private async checkForUpdates() {
const previousSessions = await this._sessionsPromise;
this._sessionsPromise = this.readSessions();
const storedSessions = await this._sessionsPromise;
const added: vscode.AuthenticationSession[] = [];
const removed: vscode.AuthenticationSession[] = [];
storedSessions.forEach(session => {
const matchesExisting = previousSessions.some(s => s.id === session.id);
// Another window added a session to the keychain, add it to our state as well
if (!matchesExisting) {
this._logger.info('Adding session found in keychain');
added.push(session);
}
});
previousSessions.forEach(session => {
const matchesExisting = storedSessions.some(s => s.id === session.id);
// Another window has logged out, remove from our state
if (!matchesExisting) {
this._logger.info('Removing session no longer found in keychain');
removed.push(session);
}
});
if (added.length || removed.length) {
this._sessionChangeEmitter.fire({ added, removed, changed: [] });
}
}
private async readSessions(): Promise<vscode.AuthenticationSession[]> {
let sessionData: SessionData[];
try {
this._logger.info('Reading sessions from keychain...');
const storedSessions = await this._keychain.getToken();
if (!storedSessions) {
return [];
}
this._logger.info('Got stored sessions!');
try {
sessionData = JSON.parse(storedSessions);
} catch (e) {
await this._keychain.deleteToken();
throw e;
}
} catch (e) {
this._logger.error(`Error reading token: ${e}`);
return [];
}
// TODO: eventually remove this Set because we should only have one session per set of scopes.
const scopesSeen = new Set<string>();
const sessionPromises = sessionData.map(async (session: SessionData) => {
// For GitHub scope list, order doesn't matter so we immediately sort the scopes
const scopesStr = [...session.scopes].sort().join(' ');
if (scopesSeen.has(scopesStr)) {
return undefined;
}
let userInfo: { id: string; accountName: string } | undefined;
if (!session.account) {
try {
userInfo = await this._githubServer.getUserInfo(session.accessToken);
this._logger.info(`Verified session with the following scopes: ${scopesStr}`);
} catch (e) {
// Remove sessions that return unauthorized response
if (e.message === 'Unauthorized') {
return undefined;
}
}
}
this._logger.trace(`Read the following session from the keychain with the following scopes: ${scopesStr}`);
scopesSeen.add(scopesStr);
return {
id: session.id,
account: {
label: session.account
? session.account.label ?? session.account.displayName ?? '<unknown>'
: userInfo?.accountName ?? '<unknown>',
id: session.account?.id ?? userInfo?.id ?? '<unknown>'
},
// we set this to session.scopes to maintain the original order of the scopes requested
// by the extension that called getSession()
scopes: session.scopes,
accessToken: session.accessToken
};
});
const verifiedSessions = (await Promise.allSettled(sessionPromises))
.filter(p => p.status === 'fulfilled')
.map(p => (p as PromiseFulfilledResult<vscode.AuthenticationSession | undefined>).value)
.filter(<T>(p?: T): p is T => Boolean(p));
this._logger.info(`Got ${verifiedSessions.length} verified sessions.`);
if (verifiedSessions.length !== sessionData.length) {
await this.storeSessions(verifiedSessions);
}
return verifiedSessions;
}
private async storeSessions(sessions: vscode.AuthenticationSession[]): Promise<void> {
this._logger.info(`Storing ${sessions.length} sessions...`);
this._sessionsPromise = Promise.resolve(sessions);
await this._keychain.setToken(JSON.stringify(sessions));
this._logger.info(`Stored ${sessions.length} sessions!`);
}
public async createSession(scopes: string[]): Promise<vscode.AuthenticationSession> {
try {
// For GitHub scope list, order doesn't matter so we use a sorted scope to determine
// if we've got a session already.
const sortedScopes = [...scopes].sort();
/* __GDPR__
"login" : {
"owner": "TylerLeonhardt",
"comment": "Used to determine how much usage the GitHub Auth Provider gets.",
"scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." }
}
*/
this._telemetryReporter?.sendTelemetryEvent('login', {
scopes: JSON.stringify(scopes),
});
const scopeString = sortedScopes.join(' ');
const token = await this._githubServer.login(scopeString);
const session = await this.tokenToSession(token, scopes);
this.afterSessionLoad(session);
const sessions = await this._sessionsPromise;
const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes));
if (sessionIndex > -1) {
sessions.splice(sessionIndex, 1, session);
} else {
sessions.push(session);
}
await this.storeSessions(sessions);
this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] });
this._logger.info('Login success!');
return session;
} catch (e) {
// If login was cancelled, do not notify user.
if (e === 'Cancelled' || e.message === 'Cancelled') {
/* __GDPR__
"loginCancelled" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users cancel the login flow." }
*/
this._telemetryReporter?.sendTelemetryEvent('loginCancelled');
throw e;
}
/* __GDPR__
"loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into an error login flow." }
*/
this._telemetryReporter?.sendTelemetryEvent('loginFailed');
vscode.window.showErrorMessage(vscode.l10n.t('Sign in failed: {0}', `${e}`));
this._logger.error(e);
throw e;
}
}
private async tokenToSession(token: string, scopes: string[]): Promise<vscode.AuthenticationSession> {
const userInfo = await this._githubServer.getUserInfo(token);
return {
id: crypto.getRandomValues(new Uint32Array(2)).reduce((prev, curr) => prev += curr.toString(16), ''),
accessToken: token,
account: { label: userInfo.accountName, id: userInfo.id },
scopes
};
}
public async removeSession(id: string) {
try {
/* __GDPR__
"logout" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out of an account." }
*/
this._telemetryReporter?.sendTelemetryEvent('logout');
this._logger.info(`Logging out of ${id}`);
const sessions = await this._sessionsPromise;
const sessionIndex = sessions.findIndex(session => session.id === id);
if (sessionIndex > -1) {
const session = sessions[sessionIndex];
sessions.splice(sessionIndex, 1);
await this.storeSessions(sessions);
await this._githubServer.logout(session);
this._sessionChangeEmitter.fire({ added: [], removed: [session], changed: [] });
} else {
this._logger.error('Session not found');
}
} catch (e) {
/* __GDPR__
"logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often logging out of an account fails." }
*/
this._telemetryReporter?.sendTelemetryEvent('logoutFailed');
vscode.window.showErrorMessage(vscode.l10n.t('Sign out failed: {0}', `${e}`));
this._logger.error(e);
throw e;
}
}
}

View File

@ -1,357 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ExperimentationTelemetry } from './common/experimentationService';
import { AuthProviderType, UriEventHandler } from './github';
import { Log } from './common/logger';
import { isSupportedClient, isSupportedTarget } from './common/env';
import { crypto } from './node/crypto';
import { fetching } from './node/fetch';
import { ExtensionHost, GitHubTarget, getFlows } from './flows';
import { NETWORK_ERROR, USER_CANCELLATION_ERROR } from './common/errors';
import { Config } from './config';
import { base64Encode } from './node/buffer';
// This is the error message that we throw if the login was cancelled for any reason. Extensions
// calling `getSession` can handle this error to know that the user cancelled the login.
const CANCELLATION_ERROR = 'Cancelled';
const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect';
const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect';
export interface IGitHubServer {
login(scopes: string): Promise<string>;
logout(session: vscode.AuthenticationSession): Promise<void>;
getUserInfo(token: string): Promise<{ id: string; accountName: string }>;
sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise<void>;
friendlyName: string;
}
export class GitHubServer implements IGitHubServer {
readonly friendlyName: string;
private readonly _type: AuthProviderType;
private _redirectEndpoint: string | undefined;
constructor(
private readonly _logger: Log,
private readonly _telemetryReporter: ExperimentationTelemetry,
private readonly _uriHandler: UriEventHandler,
private readonly _extensionKind: vscode.ExtensionKind,
private readonly _ghesUri?: vscode.Uri
) {
this._type = _ghesUri ? AuthProviderType.githubEnterprise : AuthProviderType.github;
this.friendlyName = this._type === AuthProviderType.github ? 'GitHub' : _ghesUri?.authority!;
}
get baseUri() {
if (this._type === AuthProviderType.github) {
return vscode.Uri.parse('https://github.com/');
}
return this._ghesUri!;
}
private async getRedirectEndpoint(): Promise<string> {
if (this._redirectEndpoint) {
return this._redirectEndpoint;
}
if (this._type === AuthProviderType.github) {
const proxyEndpoints = await vscode.commands.executeCommand<{ [providerId: string]: string } | undefined>('workbench.getCodeExchangeProxyEndpoints');
// If we are running in insiders vscode.dev, then ensure we use the redirect route on that.
this._redirectEndpoint = REDIRECT_URL_STABLE;
if (proxyEndpoints?.github && new URL(proxyEndpoints.github).hostname === 'insiders.vscode.dev') {
this._redirectEndpoint = REDIRECT_URL_INSIDERS;
}
} else {
// GHE only supports a single redirect endpoint, so we can't use
// insiders.vscode.dev/redirect when we're running in Insiders, unfortunately.
// Additionally, we make the assumption that this function will only be used
// in flows that target supported GHE targets, not on-prem GHES. Because of this
// assumption, we can assume that the GHE version used is at least 3.8 which is
// the version that changed the redirect endpoint to this URI from the old
// GitHub maintained server.
this._redirectEndpoint = 'https://vscode.dev/redirect';
}
return this._redirectEndpoint;
}
// TODO@joaomoreno TODO@TylerLeonhardt
private _isNoCorsEnvironment: boolean | undefined;
private async isNoCorsEnvironment(): Promise<boolean> {
if (this._isNoCorsEnvironment !== undefined) {
return this._isNoCorsEnvironment;
}
const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/dummy`));
this._isNoCorsEnvironment = (uri.scheme === 'https' && /^((insiders\.)?vscode|github)\./.test(uri.authority)) || (uri.scheme === 'http' && /^localhost/.test(uri.authority));
return this._isNoCorsEnvironment;
}
public async login(scopes: string): Promise<string> {
this._logger.info(`Logging in for the following scopes: ${scopes}`);
// Used for showing a friendlier message to the user when the explicitly cancel a flow.
let userCancelled: boolean | undefined;
const yes = vscode.l10n.t('Yes');
const no = vscode.l10n.t('No');
const promptToContinue = async (mode: string) => {
if (userCancelled === undefined) {
// We haven't had a failure yet so wait to prompt
return;
}
const message = userCancelled
? vscode.l10n.t('Having trouble logging in? Would you like to try a different way? ({0})', mode)
: vscode.l10n.t('You have not yet finished authorizing this extension to use GitHub. Would you like to try a different way? ({0})', mode);
const result = await vscode.window.showWarningMessage(message, yes, no);
if (result !== yes) {
throw new Error(CANCELLATION_ERROR);
}
};
const nonce: string = crypto.getRandomValues(new Uint32Array(2)).reduce((prev, curr) => prev += curr.toString(16), '');
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate?nonce=${encodeURIComponent(nonce)}`));
const supportedClient = isSupportedClient(callbackUri);
const supportedTarget = isSupportedTarget(this._type, this._ghesUri);
const flows = getFlows({
target: this._type === AuthProviderType.github
? GitHubTarget.DotCom
: supportedTarget ? GitHubTarget.HostedEnterprise : GitHubTarget.Enterprise,
extensionHost: typeof navigator === 'undefined'
? this._extensionKind === vscode.ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote
: ExtensionHost.WebWorker,
isSupportedClient: supportedClient
});
for (const flow of flows) {
try {
if (flow !== flows[0]) {
await promptToContinue(flow.label);
}
return await flow.trigger({
scopes,
callbackUri,
nonce,
baseUri: this.baseUri,
logger: this._logger,
uriHandler: this._uriHandler,
enterpriseUri: this._ghesUri,
redirectUri: vscode.Uri.parse(await this.getRedirectEndpoint()),
});
} catch (e) {
userCancelled = this.processLoginError(e);
}
}
throw new Error(userCancelled ? CANCELLATION_ERROR : 'No auth flow succeeded.');
}
public async logout(session: vscode.AuthenticationSession): Promise<void> {
this._logger.trace(`Deleting session (${session.id}) from server...`);
if (!Config.gitHubClientSecret) {
this._logger.warn('No client secret configured for GitHub authentication. The token has been deleted with best effort on this system, but we are unable to delete the token on server without the client secret.');
return;
}
// Only attempt to delete OAuth tokens. They are always prefixed with `gho_`.
// https://docs.github.com/en/rest/apps/oauth-applications#about-oauth-apps-and-oauth-authorizations-of-github-apps
if (!session.accessToken.startsWith('gho_')) {
this._logger.warn('The token being deleted is not an OAuth token. It has been deleted locally, but we cannot delete it on server.');
return;
}
if (!isSupportedTarget(this._type, this._ghesUri)) {
this._logger.trace('GitHub.com and GitHub hosted GitHub Enterprise are the only options that support deleting tokens on the server. Skipping.');
return;
}
const authHeader = 'Basic ' + base64Encode(`${Config.gitHubClientId}:${Config.gitHubClientSecret}`);
const uri = this.getServerUri(`/applications/${Config.gitHubClientId}/token`);
try {
// Defined here: https://docs.github.com/en/rest/apps/oauth-applications?apiVersion=2022-11-28#delete-an-app-token
const result = await fetching(uri.toString(true), {
method: 'DELETE',
headers: {
Accept: 'application/vnd.github+json',
Authorization: authHeader,
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': `${vscode.env.appName} (${vscode.env.appHost})`
},
body: JSON.stringify({ access_token: session.accessToken }),
});
if (result.status === 204) {
this._logger.trace(`Successfully deleted token from session (${session.id}) from server.`);
return;
}
try {
const body = await result.text();
throw new Error(body);
} catch (e) {
throw new Error(`${result.status} ${result.statusText}`);
}
} catch (e) {
this._logger.warn('Failed to delete token from server.' + e.message ?? e);
}
}
private getServerUri(path: string = '') {
const apiUri = this.baseUri;
// github.com and Hosted GitHub Enterprise instances
if (isSupportedTarget(this._type, this._ghesUri)) {
return vscode.Uri.parse(`${apiUri.scheme}://api.${apiUri.authority}`).with({ path });
}
// GitHub Enterprise Server (aka on-prem)
return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}/api/v3${path}`);
}
public async getUserInfo(token: string): Promise<{ id: string; accountName: string }> {
let result;
try {
this._logger.info('Getting user info...');
result = await fetching(this.getServerUri('/user').toString(), {
headers: {
Authorization: `token ${token}`,
'User-Agent': `${vscode.env.appName} (${vscode.env.appHost})`
}
});
} catch (ex) {
this._logger.error(ex.message);
throw new Error(NETWORK_ERROR);
}
if (result.ok) {
try {
const json = await result.json();
this._logger.info('Got account info!');
return { id: json.id, accountName: json.login };
} catch (e) {
this._logger.error(`Unexpected error parsing response from GitHub: ${e.message ?? e}`);
throw e;
}
} else {
// either display the response message or the http status text
let errorMessage = result.statusText;
try {
const json = await result.json();
if (json.message) {
errorMessage = json.message;
}
} catch (err) {
// noop
}
this._logger.error(`Getting account info failed: ${errorMessage}`);
throw new Error(errorMessage);
}
}
public async sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise<void> {
if (!vscode.env.isTelemetryEnabled) {
return;
}
const nocors = await this.isNoCorsEnvironment();
if (nocors) {
return;
}
if (this._type === AuthProviderType.github) {
return await this.checkUserDetails(session);
}
// GHES
await this.checkEnterpriseVersion(session.accessToken);
}
private async checkUserDetails(session: vscode.AuthenticationSession): Promise<void> {
let edu: string | undefined;
try {
const result = await fetching('https://education.github.com/api/user', {
headers: {
Authorization: `token ${session.accessToken}`,
'faculty-check-preview': 'true',
'User-Agent': `${vscode.env.appName} (${vscode.env.appHost})`
}
});
if (result.ok) {
const json: { student: boolean; faculty: boolean } = await result.json();
edu = json.student
? 'student'
: json.faculty
? 'faculty'
: 'none';
} else {
edu = 'unknown';
}
} catch (e) {
edu = 'unknown';
}
/* __GDPR__
"session" : {
"owner": "TylerLeonhardt",
"isEdu": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"isManaged": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this._telemetryReporter.sendTelemetryEvent('session', {
isEdu: edu,
// Apparently, this is how you tell if a user is an EMU...
isManaged: session.account.label.includes('_') ? 'true' : 'false'
});
}
private async checkEnterpriseVersion(token: string): Promise<void> {
try {
let version: string;
if (!isSupportedTarget(this._type, this._ghesUri)) {
const result = await fetching(this.getServerUri('/meta').toString(), {
headers: {
Authorization: `token ${token}`,
'User-Agent': `${vscode.env.appName} (${vscode.env.appHost})`
}
});
if (!result.ok) {
return;
}
const json: { verifiable_password_authentication: boolean; installed_version: string } = await result.json();
version = json.installed_version;
} else {
version = 'hosted';
}
/* __GDPR__
"ghe-session" : {
"owner": "TylerLeonhardt",
"version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this._telemetryReporter.sendTelemetryEvent('ghe-session', {
version
});
} catch {
// No-op
}
}
private processLoginError(error: Error): boolean {
if (error.message === CANCELLATION_ERROR) {
throw error;
}
this._logger.error(error.message ?? error);
return error.message === USER_CANCELLATION_ERROR;
}
}

View File

@ -1,198 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as http from 'http';
import { URL } from 'url';
import * as fs from 'fs';
import * as path from 'path';
import { randomBytes } from 'crypto';
function sendFile(res: http.ServerResponse, filepath: string) {
fs.readFile(filepath, (err, body) => {
if (err) {
console.error(err);
res.writeHead(404);
res.end();
} else {
res.writeHead(200, {
'content-length': body.length,
});
res.end(body);
}
});
}
interface IOAuthResult {
code: string;
state: string;
}
interface ILoopbackServer {
/**
* If undefined, the server is not started yet.
*/
port: number | undefined;
/**
* The nonce used
*/
nonce: string;
/**
* The state parameter used in the OAuth flow.
*/
state: string | undefined;
/**
* Starts the server.
* @returns The port to listen on.
* @throws If the server fails to start.
* @throws If the server is already started.
*/
start(): Promise<number>;
/**
* Stops the server.
* @throws If the server is not started.
* @throws If the server fails to stop.
*/
stop(): Promise<void>;
/**
* Returns a promise that resolves to the result of the OAuth flow.
*/
waitForOAuthResponse(): Promise<IOAuthResult>;
}
export class LoopbackAuthServer implements ILoopbackServer {
private readonly _server: http.Server;
private readonly _resultPromise: Promise<IOAuthResult>;
private _startingRedirect: URL;
public nonce = randomBytes(16).toString('base64');
public port: number | undefined;
public set state(state: string | undefined) {
if (state) {
this._startingRedirect.searchParams.set('state', state);
} else {
this._startingRedirect.searchParams.delete('state');
}
}
public get state(): string | undefined {
return this._startingRedirect.searchParams.get('state') ?? undefined;
}
constructor(serveRoot: string, startingRedirect: string) {
if (!serveRoot) {
throw new Error('serveRoot must be defined');
}
if (!startingRedirect) {
throw new Error('startingRedirect must be defined');
}
this._startingRedirect = new URL(startingRedirect);
let deferred: { resolve: (result: IOAuthResult) => void; reject: (reason: any) => void };
this._resultPromise = new Promise<IOAuthResult>((resolve, reject) => deferred = { resolve, reject });
this._server = http.createServer((req, res) => {
const reqUrl = new URL(req.url!, `http://${req.headers.host}`);
switch (reqUrl.pathname) {
case '/signin': {
const receivedNonce = (reqUrl.searchParams.get('nonce') ?? '').replace(/ /g, '+');
if (receivedNonce !== this.nonce) {
res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` });
res.end();
}
res.writeHead(302, { location: this._startingRedirect.toString() });
res.end();
break;
}
case '/callback': {
const code = reqUrl.searchParams.get('code') ?? undefined;
const state = reqUrl.searchParams.get('state') ?? undefined;
const nonce = (reqUrl.searchParams.get('nonce') ?? '').replace(/ /g, '+');
if (!code || !state || !nonce) {
res.writeHead(400);
res.end();
return;
}
if (this.state !== state) {
res.writeHead(302, { location: `/?error=${encodeURIComponent('State does not match.')}` });
res.end();
throw new Error('State does not match.');
}
if (this.nonce !== nonce) {
res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` });
res.end();
throw new Error('Nonce does not match.');
}
deferred.resolve({ code, state });
res.writeHead(302, { location: '/' });
res.end();
break;
}
// Serve the static files
case '/':
sendFile(res, path.join(serveRoot, 'index.html'));
break;
default:
// substring to get rid of leading '/'
sendFile(res, path.join(serveRoot, reqUrl.pathname.substring(1)));
break;
}
});
}
public start(): Promise<number> {
return new Promise<number>((resolve, reject) => {
if (this._server.listening) {
throw new Error('Server is already started');
}
const portTimeout = setTimeout(() => {
reject(new Error('Timeout waiting for port'));
}, 5000);
this._server.on('listening', () => {
const address = this._server.address();
if (typeof address === 'string') {
this.port = parseInt(address);
} else if (address instanceof Object) {
this.port = address.port;
} else {
throw new Error('Unable to determine port');
}
clearTimeout(portTimeout);
// set state which will be used to redirect back to vscode
this.state = `http://127.0.0.1:${this.port}/callback?nonce=${encodeURIComponent(this.nonce)}`;
resolve(this.port);
});
this._server.on('error', err => {
reject(new Error(`Error listening to server: ${err}`));
});
this._server.on('close', () => {
reject(new Error('Closed'));
});
this._server.listen(0, '127.0.0.1');
});
}
public stop(): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (!this._server.listening) {
throw new Error('Server is not started');
}
this._server.close((err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
public waitForOAuthResponse(): Promise<IOAuthResult> {
return this._resultPromise;
}
}

View File

@ -1,8 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function base64Encode(text: string): string {
return Buffer.from(text, 'binary').toString('base64');
}

View File

@ -1,8 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { webcrypto } from 'crypto';
export const crypto = webcrypto as any as Crypto;

View File

@ -1,7 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import fetch from 'node-fetch';
export const fetching = fetch;

View File

@ -1,196 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ExtensionHost, GitHubTarget, IFlowQuery, getFlows } from '../flows';
import { Config } from '../config';
const enum Flows {
UrlHandlerFlow = 'url handler',
LocalServerFlow = 'local server',
DeviceCodeFlow = 'device code',
PatFlow = 'personal access token'
}
suite('getFlows', () => {
let lastClientSecret: string | undefined = undefined;
suiteSetup(() => {
lastClientSecret = Config.gitHubClientSecret;
Config.gitHubClientSecret = 'asdf';
});
suiteTeardown(() => {
Config.gitHubClientSecret = lastClientSecret;
});
const testCases: Array<{ label: string; query: IFlowQuery; expectedFlows: Flows[] }> = [
{
label: 'VS Code Desktop. Local filesystem. GitHub.com',
query: {
extensionHost: ExtensionHost.Local,
isSupportedClient: true,
target: GitHubTarget.DotCom
},
expectedFlows: [
Flows.UrlHandlerFlow,
Flows.LocalServerFlow,
Flows.DeviceCodeFlow
]
},
{
label: 'VS Code Desktop. Local filesystem. GitHub Hosted Enterprise',
query: {
extensionHost: ExtensionHost.Local,
isSupportedClient: true,
target: GitHubTarget.HostedEnterprise
},
expectedFlows: [
Flows.UrlHandlerFlow,
Flows.LocalServerFlow,
Flows.DeviceCodeFlow,
Flows.PatFlow
]
},
{
label: 'VS Code Desktop. Local filesystem. GitHub Enterprise Server',
query: {
extensionHost: ExtensionHost.Local,
isSupportedClient: true,
target: GitHubTarget.Enterprise
},
expectedFlows: [
Flows.DeviceCodeFlow,
Flows.PatFlow
]
},
{
label: 'vscode.dev. serverful. GitHub.com',
query: {
extensionHost: ExtensionHost.Remote,
isSupportedClient: true,
target: GitHubTarget.DotCom
},
expectedFlows: [
Flows.UrlHandlerFlow,
Flows.DeviceCodeFlow
]
},
{
label: 'vscode.dev. serverful. GitHub Hosted Enterprise',
query: {
extensionHost: ExtensionHost.Remote,
isSupportedClient: true,
target: GitHubTarget.HostedEnterprise
},
expectedFlows: [
Flows.UrlHandlerFlow,
Flows.DeviceCodeFlow,
Flows.PatFlow
]
},
{
label: 'vscode.dev. serverful. GitHub Enterprise',
query: {
extensionHost: ExtensionHost.Remote,
isSupportedClient: true,
target: GitHubTarget.Enterprise
},
expectedFlows: [
Flows.DeviceCodeFlow,
Flows.PatFlow
]
},
{
label: 'vscode.dev. serverless. GitHub.com',
query: {
extensionHost: ExtensionHost.WebWorker,
isSupportedClient: true,
target: GitHubTarget.DotCom
},
expectedFlows: [
Flows.UrlHandlerFlow
]
},
{
label: 'vscode.dev. serverless. GitHub Hosted Enterprise',
query: {
extensionHost: ExtensionHost.WebWorker,
isSupportedClient: true,
target: GitHubTarget.HostedEnterprise
},
expectedFlows: [
Flows.UrlHandlerFlow,
Flows.PatFlow
]
},
{
label: 'vscode.dev. serverless. GitHub Enterprise Server',
query: {
extensionHost: ExtensionHost.WebWorker,
isSupportedClient: true,
target: GitHubTarget.Enterprise
},
expectedFlows: [
Flows.PatFlow
]
},
{
label: 'Code - OSS. Local filesystem. GitHub.com',
query: {
extensionHost: ExtensionHost.Local,
isSupportedClient: false,
target: GitHubTarget.DotCom
},
expectedFlows: [
Flows.LocalServerFlow,
Flows.DeviceCodeFlow,
Flows.PatFlow
]
},
{
label: 'Code - OSS. Local filesystem. GitHub Hosted Enterprise',
query: {
extensionHost: ExtensionHost.Local,
isSupportedClient: false,
target: GitHubTarget.HostedEnterprise
},
expectedFlows: [
Flows.LocalServerFlow,
Flows.DeviceCodeFlow,
Flows.PatFlow
]
},
{
label: 'Code - OSS. Local filesystem. GitHub Enterprise Server',
query: {
extensionHost: ExtensionHost.Local,
isSupportedClient: false,
target: GitHubTarget.Enterprise
},
expectedFlows: [
Flows.DeviceCodeFlow,
Flows.PatFlow
]
},
];
for (const testCase of testCases) {
test(`gives the correct flows - ${testCase.label}`, () => {
const flows = getFlows(testCase.query);
assert.strictEqual(
flows.length,
testCase.expectedFlows.length,
`Unexpected number of flows: ${flows.map(f => f.label).join(',')}`
);
for (let i = 0; i < flows.length; i++) {
const flow = flows[i];
assert.strictEqual(flow.label, testCase.expectedFlows[i]);
}
});
}
});

View File

@ -1,65 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { LoopbackAuthServer } from '../../node/authServer';
suite('LoopbackAuthServer', () => {
let server: LoopbackAuthServer;
let port: number;
setup(async () => {
server = new LoopbackAuthServer(__dirname, 'http://localhost:8080');
port = await server.start();
});
teardown(async () => {
await server.stop();
});
test('should redirect to starting redirect on /signin', async () => {
const response = await fetch(`http://localhost:${port}/signin?nonce=${server.nonce}`, {
redirect: 'manual'
});
// Redirect
assert.strictEqual(response.status, 302);
// Check location
const location = response.headers.get('location');
assert.ok(location);
const locationUrl = new URL(location);
assert.strictEqual(locationUrl.origin, 'http://localhost:8080');
// Check state
const state = locationUrl.searchParams.get('state');
assert.ok(state);
const stateLocation = new URL(state);
assert.strictEqual(stateLocation.origin, `http://127.0.0.1:${port}`);
assert.strictEqual(stateLocation.pathname, '/callback');
assert.strictEqual(stateLocation.searchParams.get('nonce'), server.nonce);
});
test('should return 400 on /callback with missing parameters', async () => {
const response = await fetch(`http://localhost:${port}/callback`);
assert.strictEqual(response.status, 400);
});
test('should resolve with code and state on /callback with valid parameters', async () => {
server.state = 'valid-state';
const response = await fetch(
`http://localhost:${port}/callback?code=valid-code&state=${server.state}&nonce=${server.nonce}`,
{ redirect: 'manual' }
);
assert.strictEqual(response.status, 302);
assert.strictEqual(response.headers.get('location'), '/');
await Promise.race([
server.waitForOAuthResponse().then(result => {
assert.strictEqual(result.code, 'valid-code');
assert.strictEqual(result.state, server.state);
}),
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000))
]);
});
});

View File

@ -1,17 +0,0 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out",
"experimentalDecorators": true,
"typeRoots": [
"./node_modules/@types"
],
"lib": [
"WebWorker"
]
},
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts"
]
}

View File

@ -1,233 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@microsoft/1ds-core-js@4.0.3", "@microsoft/1ds-core-js@^4.0.3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz#c8a92c623745a9595e06558a866658480c33bdf9"
integrity sha512-FrxNLVAPsAvD7+l63TlNS/Kodvpct2WulpDSn1dI4Xuy0kF4E2H867kHdwL/iY1Bj3zA3FSy/jvE4+OcDws7ug==
dependencies:
"@microsoft/applicationinsights-core-js" "3.0.4"
"@microsoft/applicationinsights-shims" "3.0.1"
"@microsoft/dynamicproto-js" "^2.0.2"
"@nevware21/ts-async" ">= 0.3.0 < 2.x"
"@nevware21/ts-utils" ">= 0.10.1 < 2.x"
"@microsoft/1ds-post-js@^4.0.3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-4.0.3.tgz#cfcb20bb23fb6215d3f0732f60f5b7df3e624f86"
integrity sha512-uewvmUtXKd7ttypiKQGdYI6i7UUpPkOznLayzIFrJ4r2xnG6jhPjpKRncHFXPQcM4XSWO3yf5PQ3xAbPq9t7ZQ==
dependencies:
"@microsoft/1ds-core-js" "4.0.3"
"@microsoft/applicationinsights-shims" "3.0.1"
"@microsoft/dynamicproto-js" "^2.0.2"
"@nevware21/ts-async" ">= 0.3.0 < 2.x"
"@nevware21/ts-utils" ">= 0.10.1 < 2.x"
"@microsoft/applicationinsights-channel-js@3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.4.tgz#247b6fe2158fad9826cbcdf7304f885766b36624"
integrity sha512-6TlfExmErQ8Y+/ChbkyWl+jyt4wg3T6p7lwXDsUCB0LgZmlEWMaCUS0YlT73JCWmE8j7vxW8yUm0lgsgmHns3A==
dependencies:
"@microsoft/applicationinsights-common" "3.0.4"
"@microsoft/applicationinsights-core-js" "3.0.4"
"@microsoft/applicationinsights-shims" "3.0.1"
"@microsoft/dynamicproto-js" "^2.0.2"
"@nevware21/ts-async" ">= 0.3.0 < 2.x"
"@nevware21/ts-utils" ">= 0.10.1 < 2.x"
"@microsoft/applicationinsights-common@3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.4.tgz#c4aa53ba343f5b3c7fbf54cddd3c86a5bdcd95dc"
integrity sha512-r5gWaw/K9+tKfuo2GtDiDiKASgOkPOCrKW+wZzFvuR06uuwvWjbVQ6yW/YbnfuhRF5M65ksUiMi0eCMwEOGq7Q==
dependencies:
"@microsoft/applicationinsights-core-js" "3.0.4"
"@microsoft/applicationinsights-shims" "3.0.1"
"@microsoft/dynamicproto-js" "^2.0.2"
"@nevware21/ts-utils" ">= 0.10.1 < 2.x"
"@microsoft/applicationinsights-core-js@3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.4.tgz#008308b786930d94a1de8a1fbb4af0351b74653e"
integrity sha512-anxy5kEkqBmVoEqJiJzaaXXA0wzqZi9U4zGd05xFJ04lWckP8dG3zyT3+GGdg7rDelqLTNGxndeYoFmDv63u1g==
dependencies:
"@microsoft/applicationinsights-shims" "3.0.1"
"@microsoft/dynamicproto-js" "^2.0.2"
"@nevware21/ts-async" ">= 0.3.0 < 2.x"
"@nevware21/ts-utils" ">= 0.10.1 < 2.x"
"@microsoft/applicationinsights-shims@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz#3865b73ace8405b9c4618cc5c571f2fe3876f06f"
integrity sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==
dependencies:
"@nevware21/ts-utils" ">= 0.9.4 < 2.x"
"@microsoft/applicationinsights-web-basic@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.4.tgz#9a23323276b4a5a0dc6a352e2de5d75e3c16b534"
integrity sha512-KfoxPlLlf0JT12ADb23C5iGye/yFouoMgHEKULxkSQcYY9SsW/8rVrqqvoYKAL+u215CZU2A8Kc8sR3ehEaPCQ==
dependencies:
"@microsoft/applicationinsights-channel-js" "3.0.4"
"@microsoft/applicationinsights-common" "3.0.4"
"@microsoft/applicationinsights-core-js" "3.0.4"
"@microsoft/applicationinsights-shims" "3.0.1"
"@microsoft/dynamicproto-js" "^2.0.2"
"@nevware21/ts-async" ">= 0.3.0 < 2.x"
"@nevware21/ts-utils" ">= 0.10.1 < 2.x"
"@microsoft/dynamicproto-js@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz#e57fbec2e7067d48b7e8e1e1c1d354028ef718a6"
integrity sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==
dependencies:
"@nevware21/ts-utils" ">= 0.9.4 < 2.x"
"@nevware21/ts-async@>= 0.3.0 < 2.x":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@nevware21/ts-async/-/ts-async-0.3.0.tgz#a8b97ba01065fc930de9a3f4dd4a05e862becc6c"
integrity sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==
dependencies:
"@nevware21/ts-utils" ">= 0.10.0 < 2.x"
"@nevware21/ts-utils@>= 0.10.0 < 2.x", "@nevware21/ts-utils@>= 0.10.1 < 2.x", "@nevware21/ts-utils@>= 0.9.4 < 2.x":
version "0.10.1"
resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7"
integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==
"@types/mocha@^9.1.1":
version "9.1.1"
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4"
integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==
"@types/node-fetch@^2.5.7":
version "2.5.7"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c"
integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*":
version "14.0.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b"
integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA==
"@types/node@18.x":
version "18.15.13"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469"
integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==
"@vscode/extension-telemetry@^0.9.0":
version "0.9.0"
resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz#8c6c61e253ff304f46045f04edd60059b144417a"
integrity sha512-37RxGHXrs3GoXPgCUKQhghEu0gxs8j27RLjQwwtSf4WhPdJKz8UrqMYzpsXlliQ05zURYmtdGZst9C6+hfWXaQ==
dependencies:
"@microsoft/1ds-core-js" "^4.0.3"
"@microsoft/1ds-post-js" "^4.0.3"
"@microsoft/applicationinsights-web-basic" "^3.0.4"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
axios@^1.6.1:
version "1.6.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2"
integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
follow-redirects@^1.15.0:
version "1.15.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
form-data@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
mime-db@1.44.0:
version "1.44.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
mime-types@^2.1.12:
version "2.1.27"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
dependencies:
mime-db "1.44.0"
node-fetch@2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
tas-client@0.1.73:
version "0.1.73"
resolved "https://registry.yarnpkg.com/tas-client/-/tas-client-0.1.73.tgz#2dacf68547a37989ef1554c6510dc108a1ea7a71"
integrity sha512-UDdUF9kV2hYdlv+7AgqP2kXarVSUhjK7tg1BUflIRGEgND0/QoNpN64rcEuhEcM8AIbW65yrCopJWqRhLZ3m8w==
dependencies:
axios "^1.6.1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
vscode-tas-client@^0.1.47:
version "0.1.75"
resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.75.tgz#771780a9a178163028299f52d41973300060dd38"
integrity sha512-/+ALFWPI4U3obeRvLFSt39guT7P9bZQrkmcLoiS+2HtzJ/7iPKNt5Sj+XTiitGlPYVFGFc0plxX8AAp6Uxs0xQ==
dependencies:
tas-client "0.1.73"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"

View File

@ -1,7 +0,0 @@
src/**
!src/common/config.json
out/**
build/**
extension.webpack.config.js
tsconfig.json
yarn.lock

View File

@ -1,12 +0,0 @@
# GitHub for Visual Studio Code
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
## Features
This extension provides the following GitHub-related features for VS Code:
- `Publish to GitHub` command
- `Clone from GitHub` participant to the `Git: Clone` command
- GitHub authentication for built-in git commands, controlled via the `github.gitAuthentication` command
- Automatic fork creation when attempting to push to a repository without permissions

View File

@ -1,17 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withDefaults = require('../shared.webpack.config');
module.exports = withDefaults({
context: __dirname,
entry: {
extension: './src/extension.ts'
}
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,9 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.vscode-dark img[src$=\#gh-light-mode-only],
.vscode-light img[src$=\#gh-dark-mode-only] {
display: none;
}

View File

@ -1,195 +0,0 @@
{
"name": "github",
"displayName": "%displayName%",
"description": "%description%",
"publisher": "vscode",
"license": "MIT",
"version": "0.0.1",
"engines": {
"vscode": "^1.41.0"
},
"aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
"icon": "images/icon.png",
"categories": [
"Other"
],
"activationEvents": [
"*"
],
"extensionDependencies": [
"vscode.git-base"
],
"main": "./out/extension.js",
"capabilities": {
"virtualWorkspaces": false,
"untrustedWorkspaces": {
"supported": true
}
},
"enabledApiProposals": [
"contribShareMenu",
"contribEditSessions",
"canonicalUriProvider",
"shareProvider"
],
"contributes": {
"commands": [
{
"command": "github.publish",
"title": "Publish to GitHub"
},
{
"command": "github.copyVscodeDevLink",
"title": "Copy vscode.dev Link"
},
{
"command": "github.copyVscodeDevLinkFile",
"title": "Copy vscode.dev Link"
},
{
"command": "github.copyVscodeDevLinkWithoutRange",
"title": "Copy vscode.dev Link"
},
{
"command": "github.openOnVscodeDev",
"title": "Open in vscode.dev",
"icon": "$(globe)"
}
],
"continueEditSession": [
{
"command": "github.openOnVscodeDev",
"when": "github.hasGitHubRepo",
"qualifiedName": "Continue Working in vscode.dev",
"category": "Remote Repositories",
"remoteGroup": "virtualfs_44_vscode-vfs_2_web@2"
}
],
"menus": {
"commandPalette": [
{
"command": "github.publish",
"when": "git-base.gitEnabled && remoteName != 'codespaces'"
},
{
"command": "github.copyVscodeDevLink",
"when": "false"
},
{
"command": "github.copyVscodeDevLinkFile",
"when": "false"
},
{
"command": "github.copyVscodeDevLinkWithoutRange",
"when": "false"
},
{
"command": "github.openOnVscodeDev",
"when": "false"
}
],
"file/share": [
{
"command": "github.copyVscodeDevLinkFile",
"when": "github.hasGitHubRepo && remoteName != 'codespaces'",
"group": "0_vscode@0"
}
],
"editor/context/share": [
{
"command": "github.copyVscodeDevLink",
"when": "github.hasGitHubRepo && resourceScheme != untitled && remoteName != 'codespaces'",
"group": "0_vscode@0"
}
],
"explorer/context/share": [
{
"command": "github.copyVscodeDevLinkWithoutRange",
"when": "github.hasGitHubRepo && resourceScheme != untitled && remoteName != 'codespaces'",
"group": "0_vscode@0"
}
],
"editor/lineNumber/context": [
{
"command": "github.copyVscodeDevLink",
"when": "github.hasGitHubRepo && resourceScheme != untitled && activeEditor == workbench.editors.files.textFileEditor && config.editor.lineNumbers == on && remoteName != 'codespaces'",
"group": "1_cutcopypaste@2"
},
{
"command": "github.copyVscodeDevLink",
"when": "github.hasGitHubRepo && resourceScheme != untitled && activeEditor == workbench.editor.notebook && remoteName != 'codespaces'",
"group": "1_cutcopypaste@2"
}
],
"editor/title/context/share": [
{
"command": "github.copyVscodeDevLinkWithoutRange",
"when": "github.hasGitHubRepo && resourceScheme != untitled && remoteName != 'codespaces'",
"group": "0_vscode@0"
}
]
},
"configuration": [
{
"title": "GitHub",
"properties": {
"github.branchProtection": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "%config.branchProtection%"
},
"github.gitAuthentication": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "%config.gitAuthentication%"
},
"github.gitProtocol": {
"type": "string",
"enum": [
"https",
"ssh"
],
"default": "https",
"description": "%config.gitProtocol%"
}
}
}
],
"viewsWelcome": [
{
"view": "scm",
"contents": "%welcome.publishFolder%",
"when": "config.git.enabled && git.state == initialized && workbenchState == folder && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0"
},
{
"view": "scm",
"contents": "%welcome.publishWorkspaceFolder%",
"when": "config.git.enabled && git.state == initialized && workbenchState == workspace && workspaceFolderCount != 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0"
}
],
"markdown.previewStyles": [
"./markdown.css"
]
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "gulp compile-extension:github",
"watch": "gulp watch-extension:github"
},
"dependencies": {
"@octokit/graphql": "5.0.5",
"@octokit/graphql-schema": "14.4.0",
"@octokit/rest": "19.0.4",
"tunnel": "^0.0.6",
"@vscode/extension-telemetry": "^0.9.0"
},
"devDependencies": {
"@types/node": "18.x"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/vscode.git"
}
}

View File

@ -1,27 +0,0 @@
{
"displayName": "GitHub",
"description": "GitHub features for Kylin-Code",
"config.branchProtection": "Controls whether to query repository rules for GitHub repositories",
"config.gitAuthentication": "Controls whether to enable automatic GitHub authentication for git commands within Kylin-Code.",
"config.gitProtocol": "Controls which protocol is used to clone a GitHub repository",
"welcome.publishFolder": {
"message": "You can directly publish this folder to a GitHub repository. Once published, you'll have access to source control features powered by git and GitHub.\n[$(github) Publish to GitHub](command:github.publish)",
"comment": [
"{Locked='$(github)'}",
"Do not translate '$(github)'. It will be rendered as an icon",
"{Locked='](command:github.publish'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for Kylin-Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"welcome.publishWorkspaceFolder": {
"message": "You can directly publish a workspace folder to a GitHub repository. Once published, you'll have access to source control features powered by git and GitHub.\n[$(github) Publish to GitHub](command:github.publish)",
"comment": [
"{Locked='$(github)'}",
"Do not translate '$(github)'. It will be rendered as an icon",
"{Locked='](command:github.publish'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for Kylin-Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
}
}

View File

@ -1,89 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AuthenticationSession, authentication, window } from 'vscode';
import { Agent, globalAgent } from 'https';
import { graphql } from '@octokit/graphql/dist-types/types';
import { Octokit } from '@octokit/rest';
import { httpsOverHttp } from 'tunnel';
import { URL } from 'url';
export class AuthenticationError extends Error { }
function getAgent(url: string | undefined = process.env.HTTPS_PROXY): Agent {
if (!url) {
return globalAgent;
}
try {
const { hostname, port, username, password } = new URL(url);
const auth = username && password && `${username}:${password}`;
return httpsOverHttp({ proxy: { host: hostname, port, proxyAuth: auth } });
} catch (e) {
window.showErrorMessage(`HTTPS_PROXY environment variable ignored: ${e.message}`);
return globalAgent;
}
}
const scopes = ['repo', 'workflow', 'user:email', 'read:user'];
export async function getSession(): Promise<AuthenticationSession> {
return await authentication.getSession('github', scopes, { createIfNone: true });
}
let _octokit: Promise<Octokit> | undefined;
export function getOctokit(): Promise<Octokit> {
if (!_octokit) {
_octokit = getSession().then(async session => {
const token = session.accessToken;
const agent = getAgent();
const { Octokit } = await import('@octokit/rest');
return new Octokit({
request: { agent },
userAgent: 'GitHub VSCode',
auth: `token ${token}`
});
}).then(null, async err => {
_octokit = undefined;
throw err;
});
}
return _octokit;
}
let _octokitGraphql: Promise<graphql> | undefined;
export async function getOctokitGraphql(): Promise<graphql> {
if (!_octokitGraphql) {
try {
const session = await authentication.getSession('github', scopes, { silent: true });
if (!session) {
throw new AuthenticationError('No GitHub authentication session available.');
}
const token = session.accessToken;
const { graphql } = await import('@octokit/graphql');
return graphql.defaults({
headers: {
authorization: `token ${token}`
},
request: {
agent: getAgent()
}
});
} catch (err) {
_octokitGraphql = undefined;
throw err;
}
}
return _octokitGraphql;
}

View File

@ -1,244 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { authentication, EventEmitter, LogOutputChannel, Memento, Uri, workspace } from 'vscode';
import { Repository as GitHubRepository, RepositoryRuleset } from '@octokit/graphql-schema';
import { AuthenticationError, getOctokitGraphql } from './auth';
import { API, BranchProtection, BranchProtectionProvider, BranchProtectionRule, Repository } from './typings/git';
import { DisposableStore, getRepositoryFromUrl } from './util';
import TelemetryReporter from '@vscode/extension-telemetry';
const REPOSITORY_QUERY = `
query repositoryPermissions($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
defaultBranchRef {
name
},
viewerPermission
}
}
`;
const REPOSITORY_RULESETS_QUERY = `
query repositoryRulesets($owner: String!, $repo: String!, $cursor: String, $limit: Int = 100) {
repository(owner: $owner, name: $repo) {
rulesets(includeParents: true, first: $limit, after: $cursor) {
nodes {
name
enforcement
rules(type: PULL_REQUEST) {
totalCount
}
conditions {
refName {
include
exclude
}
}
target
},
pageInfo {
endCursor,
hasNextPage
}
}
}
}
`;
export class GithubBranchProtectionProviderManager {
private readonly disposables = new DisposableStore();
private readonly providerDisposables = new DisposableStore();
private _enabled = false;
private set enabled(enabled: boolean) {
if (this._enabled === enabled) {
return;
}
if (enabled) {
for (const repository of this.gitAPI.repositories) {
this.providerDisposables.add(this.gitAPI.registerBranchProtectionProvider(repository.rootUri, new GithubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter)));
}
} else {
this.providerDisposables.dispose();
}
this._enabled = enabled;
}
constructor(
private readonly gitAPI: API,
private readonly globalState: Memento,
private readonly logger: LogOutputChannel,
private readonly telemetryReporter: TelemetryReporter) {
this.disposables.add(this.gitAPI.onDidOpenRepository(repository => {
if (this._enabled) {
this.providerDisposables.add(gitAPI.registerBranchProtectionProvider(repository.rootUri, new GithubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter)));
}
}));
this.disposables.add(workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('github.branchProtection')) {
this.updateEnablement();
}
}));
this.updateEnablement();
}
private updateEnablement(): void {
const config = workspace.getConfiguration('github', null);
this.enabled = config.get<boolean>('branchProtection', true) === true;
}
dispose(): void {
this.enabled = false;
this.disposables.dispose();
}
}
export class GithubBranchProtectionProvider implements BranchProtectionProvider {
private readonly _onDidChangeBranchProtection = new EventEmitter<Uri>();
onDidChangeBranchProtection = this._onDidChangeBranchProtection.event;
private branchProtection: BranchProtection[];
private readonly globalStateKey = `branchProtection:${this.repository.rootUri.toString()}`;
constructor(
private readonly repository: Repository,
private readonly globalState: Memento,
private readonly logger: LogOutputChannel,
private readonly telemetryReporter: TelemetryReporter) {
// Restore branch protection from global state
this.branchProtection = this.globalState.get<BranchProtection[]>(this.globalStateKey, []);
repository.status().then(() => {
authentication.onDidChangeSessions(e => {
if (e.provider.id === 'github') {
this.updateRepositoryBranchProtection();
}
});
this.updateRepositoryBranchProtection();
});
}
provideBranchProtection(): BranchProtection[] {
return this.branchProtection;
}
private async getRepositoryDetails(owner: string, repo: string): Promise<GitHubRepository> {
const graphql = await getOctokitGraphql();
const { repository } = await graphql<{ repository: GitHubRepository }>(REPOSITORY_QUERY, { owner, repo });
return repository;
}
private async getRepositoryRulesets(owner: string, repo: string): Promise<RepositoryRuleset[]> {
const rulesets: RepositoryRuleset[] = [];
let cursor: string | undefined = undefined;
const graphql = await getOctokitGraphql();
while (true) {
const { repository } = await graphql<{ repository: GitHubRepository }>(REPOSITORY_RULESETS_QUERY, { owner, repo, cursor });
rulesets.push(...(repository.rulesets?.nodes ?? [])
// Active branch ruleset that contains the pull request required rule
.filter(node => node && node.target === 'BRANCH' && node.enforcement === 'ACTIVE' && (node.rules?.totalCount ?? 0) > 0) as RepositoryRuleset[]);
if (repository.rulesets?.pageInfo.hasNextPage) {
cursor = repository.rulesets.pageInfo.endCursor as string | undefined;
} else {
break;
}
}
return rulesets;
}
private async updateRepositoryBranchProtection(): Promise<void> {
const branchProtection: BranchProtection[] = [];
try {
for (const remote of this.repository.state.remotes) {
const repository = getRepositoryFromUrl(remote.pushUrl ?? remote.fetchUrl ?? '');
if (!repository) {
continue;
}
// Repository details
this.logger.trace(`Fetching repository details for "${repository.owner}/${repository.repo}".`);
const repositoryDetails = await this.getRepositoryDetails(repository.owner, repository.repo);
// Check repository write permission
if (repositoryDetails.viewerPermission !== 'ADMIN' && repositoryDetails.viewerPermission !== 'MAINTAIN' && repositoryDetails.viewerPermission !== 'WRITE') {
this.logger.trace(`Skipping branch protection for "${repository.owner}/${repository.repo}" due to missing repository write permission.`);
continue;
}
// Get repository rulesets
const branchProtectionRules: BranchProtectionRule[] = [];
const repositoryRulesets = await this.getRepositoryRulesets(repository.owner, repository.repo);
for (const ruleset of repositoryRulesets) {
branchProtectionRules.push({
include: (ruleset.conditions.refName?.include ?? []).map(r => this.parseRulesetRefName(repositoryDetails, r)),
exclude: (ruleset.conditions.refName?.exclude ?? []).map(r => this.parseRulesetRefName(repositoryDetails, r))
});
}
branchProtection.push({ remote: remote.name, rules: branchProtectionRules });
}
this.branchProtection = branchProtection;
this._onDidChangeBranchProtection.fire(this.repository.rootUri);
// Save branch protection to global state
await this.globalState.update(this.globalStateKey, branchProtection);
this.logger.trace(`Branch protection for "${this.repository.rootUri.toString()}": ${JSON.stringify(branchProtection)}.`);
/* __GDPR__
"branchProtection" : {
"owner": "lszomoru",
"rulesetCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of repository rulesets" }
}
*/
this.telemetryReporter.sendTelemetryEvent('branchProtection', undefined, { rulesetCount: this.branchProtection.length });
} catch (err) {
this.logger.warn(`Failed to update repository branch protection: ${err.message}`);
if (err instanceof AuthenticationError) {
// A GitHub authentication session could be missing if the user has not yet
// signed in with their GitHub account or they have signed out. If there is
// branch protection information we have to clear it.
if (this.branchProtection.length !== 0) {
this.branchProtection = branchProtection;
this._onDidChangeBranchProtection.fire(this.repository.rootUri);
await this.globalState.update(this.globalStateKey, undefined);
}
}
}
}
private parseRulesetRefName(repository: GitHubRepository, refName: string): string {
if (refName.startsWith('refs/heads/')) {
return refName.substring(11);
}
switch (refName) {
case '~ALL':
return '**/*';
case '~DEFAULT_BRANCH':
return repository.defaultBranchRef!.name;
default:
return refName;
}
}
}

View File

@ -1,49 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, CanonicalUriProvider, CanonicalUriRequestOptions, Disposable, ProviderResult, Uri, workspace } from 'vscode';
import { API } from './typings/git';
const SUPPORTED_SCHEMES = ['ssh', 'https', 'file'];
export class GitHubCanonicalUriProvider implements CanonicalUriProvider {
private disposables: Disposable[] = [];
constructor(private gitApi: API) {
this.disposables.push(...SUPPORTED_SCHEMES.map((scheme) => workspace.registerCanonicalUriProvider(scheme, this)));
}
dispose() { this.disposables.forEach((disposable) => disposable.dispose()); }
provideCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, _token: CancellationToken): ProviderResult<Uri> {
if (options.targetScheme !== 'https') {
return;
}
switch (uri.scheme) {
case 'file': {
const repository = this.gitApi.getRepository(uri);
const remote = repository?.state.remotes.find((remote) => remote.name === repository.state.HEAD?.remote)?.pushUrl?.replace(/^(git@[^\/:]+)(:)/i, 'ssh://$1/');
if (remote) {
return toHttpsGitHubRemote(uri);
}
}
default:
return toHttpsGitHubRemote(uri);
}
}
}
function toHttpsGitHubRemote(uri: Uri) {
if (uri.scheme === 'ssh' && uri.authority === 'git@github.com') {
// if this is a git@github.com URI, return the HTTPS equivalent
const [owner, repo] = (uri.path.endsWith('.git') ? uri.path.slice(0, -4) : uri.path).split('/').filter((segment) => segment.length > 0);
return Uri.parse(`https://github.com/${owner}/${repo}`);
}
if (uri.scheme === 'https' && uri.authority === 'github.com') {
return uri;
}
return undefined;
}

View File

@ -1,65 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { API as GitAPI } from './typings/git';
import { publishRepository } from './publish';
import { DisposableStore } from './util';
import { LinkContext, getLink, getVscodeDevHost } from './links';
async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean, context: LinkContext, includeRange = true) {
try {
const permalink = await getLink(gitAPI, useSelection, true, getVscodeDevHost(), 'headlink', context, includeRange);
if (permalink) {
return vscode.env.clipboard.writeText(permalink);
}
} catch (err) {
if (!(err instanceof vscode.CancellationError)) {
vscode.window.showErrorMessage(err.message);
}
}
}
async function openVscodeDevLink(gitAPI: GitAPI): Promise<vscode.Uri | undefined> {
try {
const headlink = await getLink(gitAPI, true, false, getVscodeDevHost(), 'headlink');
return headlink ? vscode.Uri.parse(headlink) : undefined;
} catch (err) {
if (!(err instanceof vscode.CancellationError)) {
vscode.window.showErrorMessage(err.message);
}
return undefined;
}
}
export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
const disposables = new DisposableStore();
disposables.add(vscode.commands.registerCommand('github.publish', async () => {
try {
publishRepository(gitAPI);
} catch (err) {
vscode.window.showErrorMessage(err.message);
}
}));
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLink', async (context: LinkContext) => {
return copyVscodeDevLink(gitAPI, true, context);
}));
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLinkFile', async (context: LinkContext) => {
return copyVscodeDevLink(gitAPI, false, context);
}));
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLinkWithoutRange', async (context: LinkContext) => {
return copyVscodeDevLink(gitAPI, true, context, false);
}));
disposables.add(vscode.commands.registerCommand('github.openOnVscodeDev', async () => {
return openVscodeDevLink(gitAPI);
}));
return disposables;
}

View File

@ -1,64 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CredentialsProvider, Credentials, API as GitAPI } from './typings/git';
import { workspace, Uri, Disposable } from 'vscode';
import { getSession } from './auth';
const EmptyDisposable: Disposable = { dispose() { } };
class GitHubCredentialProvider implements CredentialsProvider {
async getCredentials(host: Uri): Promise<Credentials | undefined> {
if (!/github\.com/i.test(host.authority)) {
return;
}
const session = await getSession();
return { username: session.account.id, password: session.accessToken };
}
}
export class GithubCredentialProviderManager {
private providerDisposable: Disposable = EmptyDisposable;
private readonly disposable: Disposable;
private _enabled = false;
private set enabled(enabled: boolean) {
if (this._enabled === enabled) {
return;
}
this._enabled = enabled;
if (enabled) {
this.providerDisposable = this.gitAPI.registerCredentialsProvider(new GitHubCredentialProvider());
} else {
this.providerDisposable.dispose();
}
}
constructor(private gitAPI: GitAPI) {
this.disposable = workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('github')) {
this.refresh();
}
});
this.refresh();
}
private refresh(): void {
const config = workspace.getConfiguration('github', null);
const enabled = config.get<boolean>('gitAuthentication', true);
this.enabled = !!enabled;
}
dispose(): void {
this.enabled = false;
this.disposable.dispose();
}
}

View File

@ -1,132 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { commands, Disposable, ExtensionContext, extensions, l10n, LogLevel, LogOutputChannel, window } from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import { GithubRemoteSourceProvider } from './remoteSourceProvider';
import { API, GitExtension } from './typings/git';
import { registerCommands } from './commands';
import { GithubCredentialProviderManager } from './credentialProvider';
import { DisposableStore, repositoryHasGitHubRemote } from './util';
import { GithubPushErrorHandler } from './pushErrorHandler';
import { GitBaseExtension } from './typings/git-base';
import { GithubRemoteSourcePublisher } from './remoteSourcePublisher';
import { GithubBranchProtectionProviderManager } from './branchProtection';
import { GitHubCanonicalUriProvider } from './canonicalUriProvider';
import { VscodeDevShareProvider } from './shareProviders';
export function activate(context: ExtensionContext): void {
const disposables: Disposable[] = [];
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));
const logger = window.createOutputChannel('GitHub', { log: true });
disposables.push(logger);
const onDidChangeLogLevel = (logLevel: LogLevel) => {
logger.appendLine(l10n.t('Log level: {0}', LogLevel[logLevel]));
};
disposables.push(logger.onDidChangeLogLevel(onDidChangeLogLevel));
onDidChangeLogLevel(logger.logLevel);
const { aiKey } = require('../package.json') as { aiKey: string };
const telemetryReporter = new TelemetryReporter(aiKey);
disposables.push(telemetryReporter);
disposables.push(initializeGitBaseExtension());
disposables.push(initializeGitExtension(context, telemetryReporter, logger));
}
function initializeGitBaseExtension(): Disposable {
const disposables = new DisposableStore();
const initialize = () => {
try {
const gitBaseAPI = gitBaseExtension.getAPI(1);
disposables.add(gitBaseAPI.registerRemoteSourceProvider(new GithubRemoteSourceProvider()));
}
catch (err) {
console.error('Could not initialize GitHub extension');
console.warn(err);
}
};
const onDidChangeGitBaseExtensionEnablement = (enabled: boolean) => {
if (!enabled) {
disposables.dispose();
} else {
initialize();
}
};
const gitBaseExtension = extensions.getExtension<GitBaseExtension>('vscode.git-base')!.exports;
disposables.add(gitBaseExtension.onDidChangeEnablement(onDidChangeGitBaseExtensionEnablement));
onDidChangeGitBaseExtensionEnablement(gitBaseExtension.enabled);
return disposables;
}
function setGitHubContext(gitAPI: API, disposables: DisposableStore) {
if (gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
} else {
const openRepoDisposable = gitAPI.onDidOpenRepository(async e => {
await e.status();
if (repositoryHasGitHubRemote(e)) {
commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
openRepoDisposable.dispose();
}
});
disposables.add(openRepoDisposable);
}
}
function initializeGitExtension(context: ExtensionContext, telemetryReporter: TelemetryReporter, logger: LogOutputChannel): Disposable {
const disposables = new DisposableStore();
let gitExtension = extensions.getExtension<GitExtension>('vscode.git');
const initialize = () => {
gitExtension!.activate()
.then(extension => {
const onDidChangeGitExtensionEnablement = (enabled: boolean) => {
if (enabled) {
const gitAPI = extension.getAPI(1);
disposables.add(registerCommands(gitAPI));
disposables.add(new GithubCredentialProviderManager(gitAPI));
disposables.add(new GithubBranchProtectionProviderManager(gitAPI, context.globalState, logger, telemetryReporter));
disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler(telemetryReporter)));
disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI)));
disposables.add(new GitHubCanonicalUriProvider(gitAPI));
disposables.add(new VscodeDevShareProvider(gitAPI));
setGitHubContext(gitAPI, disposables);
commands.executeCommand('setContext', 'git-base.gitEnabled', true);
} else {
disposables.dispose();
}
};
disposables.add(extension.onDidChangeEnablement(onDidChangeGitExtensionEnablement));
onDidChangeGitExtensionEnablement(extension.enabled);
});
};
if (gitExtension) {
initialize();
} else {
const listener = extensions.onDidChange(() => {
if (!gitExtension && extensions.getExtension<GitExtension>('vscode.git')) {
gitExtension = extensions.getExtension<GitExtension>('vscode.git');
initialize();
listener.dispose();
}
});
disposables.add(listener);
}
return disposables;
}

View File

@ -1,256 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { API as GitAPI, RefType, Repository } from './typings/git';
import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util';
export function isFileInRepo(repository: Repository, file: vscode.Uri): boolean {
return file.path.toLowerCase() === repository.rootUri.path.toLowerCase() ||
(file.path.toLowerCase().startsWith(repository.rootUri.path.toLowerCase()) &&
file.path.substring(repository.rootUri.path.length).startsWith('/'));
}
export function getRepositoryForFile(gitAPI: GitAPI, file: vscode.Uri): Repository | undefined {
for (const repository of gitAPI.repositories) {
if (isFileInRepo(repository, file)) {
return repository;
}
}
return undefined;
}
enum LinkType {
File = 1,
Notebook = 2
}
interface IFilePosition {
type: LinkType.File;
uri: vscode.Uri;
range: vscode.Range | undefined;
}
interface INotebookPosition {
type: LinkType.Notebook;
uri: vscode.Uri;
cellIndex: number;
range: vscode.Range | undefined;
}
interface EditorLineNumberContext {
uri: vscode.Uri;
lineNumber: number;
}
export type LinkContext = vscode.Uri | EditorLineNumberContext | undefined;
function extractContext(context: LinkContext): { fileUri: vscode.Uri | undefined; lineNumber: number | undefined } {
if (context instanceof vscode.Uri) {
return { fileUri: context, lineNumber: undefined };
} else if (context !== undefined && 'lineNumber' in context && 'uri' in context) {
return { fileUri: context.uri, lineNumber: context.lineNumber };
} else {
return { fileUri: undefined, lineNumber: undefined };
}
}
function getFileAndPosition(context: LinkContext): IFilePosition | INotebookPosition | undefined {
let range: vscode.Range | undefined;
const { fileUri, lineNumber } = extractContext(context);
const uri = fileUri ?? vscode.window.activeTextEditor?.document.uri;
if (uri) {
if (uri.scheme === 'vscode-notebook-cell' && vscode.window.activeNotebookEditor?.notebook.uri.fsPath === uri.fsPath) {
// if the active editor is a notebook editor and the focus is inside any a cell text editor
// generate deep link for text selection for the notebook cell.
const cell = vscode.window.activeNotebookEditor.notebook.getCells().find(cell => cell.document.uri.fragment === uri?.fragment);
const cellIndex = cell?.index ?? vscode.window.activeNotebookEditor.selection.start;
const range = getRangeOrSelection(lineNumber);
return { type: LinkType.Notebook, uri, cellIndex, range };
} else {
// the active editor is a text editor
range = getRangeOrSelection(lineNumber);
return { type: LinkType.File, uri, range };
}
}
if (vscode.window.activeNotebookEditor) {
// if the active editor is a notebook editor but the focus is not inside any cell text editor, generate deep link for the cell selection in the notebook document.
return { type: LinkType.Notebook, uri: vscode.window.activeNotebookEditor.notebook.uri, cellIndex: vscode.window.activeNotebookEditor.selection.start, range: undefined };
}
return undefined;
}
function getRangeOrSelection(lineNumber: number | undefined) {
return lineNumber !== undefined && (!vscode.window.activeTextEditor || vscode.window.activeTextEditor.selection.isEmpty || !vscode.window.activeTextEditor.selection.contains(new vscode.Position(lineNumber - 1, 0)))
? new vscode.Range(lineNumber - 1, 0, lineNumber - 1, 1)
: vscode.window.activeTextEditor?.selection;
}
export function rangeString(range: vscode.Range | undefined) {
if (!range) {
return '';
}
let hash = `#L${range.start.line + 1}`;
if (range.start.line !== range.end.line) {
hash += `-L${range.end.line + 1}`;
}
return hash;
}
export function notebookCellRangeString(index: number | undefined, range: vscode.Range | undefined) {
if (index === undefined) {
return '';
}
if (!range) {
return `#C${index + 1}`;
}
let hash = `#C${index + 1}:L${range.start.line + 1}`;
if (range.start.line !== range.end.line) {
hash += `-L${range.end.line + 1}`;
}
return hash;
}
export function encodeURIComponentExceptSlashes(path: string) {
// There may be special characters like # and whitespace in the path.
// These characters are not escaped by encodeURI(), so it is not sufficient to
// feed the full URI to encodeURI().
// Additonally, if we feed the full path into encodeURIComponent(),
// this will also encode the path separators, leading to an invalid path.
// Therefore, split on the path separator and encode each segment individually.
return path.split('/').map((segment) => encodeURIComponent(segment)).join('/');
}
export async function getLink(gitAPI: GitAPI, useSelection: boolean, shouldEnsurePublished: boolean, hostPrefix?: string, linkType: 'permalink' | 'headlink' = 'permalink', context?: LinkContext, useRange?: boolean): Promise<string | undefined> {
hostPrefix = hostPrefix ?? 'https://github.com';
const fileAndPosition = getFileAndPosition(context);
const fileUri = fileAndPosition?.uri;
// Use the first repo if we cannot determine a repo from the uri.
const githubRepository = gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo));
const gitRepo = (fileUri ? getRepositoryForFile(gitAPI, fileUri) : githubRepository) ?? githubRepository;
if (!gitRepo) {
return;
}
if (shouldEnsurePublished && fileUri) {
await ensurePublished(gitRepo, fileUri);
}
let repo: { owner: string; repo: string } | undefined;
gitRepo.state.remotes.find(remote => {
if (remote.fetchUrl) {
const foundRepo = getRepositoryFromUrl(remote.fetchUrl);
if (foundRepo && (remote.name === gitRepo.state.HEAD?.upstream?.remote)) {
repo = foundRepo;
return;
} else if (foundRepo && !repo) {
repo = foundRepo;
}
}
return;
});
if (!repo) {
return;
}
const blobSegment = gitRepo.state.HEAD ? (`/blob/${linkType === 'headlink' && gitRepo.state.HEAD.name ? encodeURIComponentExceptSlashes(gitRepo.state.HEAD.name) : gitRepo.state.HEAD?.commit}`) : '';
const uriWithoutFileSegments = `${hostPrefix}/${repo.owner}/${repo.repo}${blobSegment}`;
if (!fileUri) {
return uriWithoutFileSegments;
}
const encodedFilePath = encodeURIComponentExceptSlashes(fileUri.path.substring(gitRepo.rootUri.path.length));
const fileSegments = fileAndPosition.type === LinkType.File
? (useSelection ? `${encodedFilePath}${useRange ? rangeString(fileAndPosition.range) : ''}` : '')
: (useSelection ? `${encodedFilePath}${useRange ? notebookCellRangeString(fileAndPosition.cellIndex, fileAndPosition.range) : ''}` : '');
return `${uriWithoutFileSegments}${fileSegments}`;
}
export function getBranchLink(url: string, branch: string, hostPrefix: string = 'https://github.com') {
const repo = getRepositoryFromUrl(url);
if (!repo) {
throw new Error('Invalid repository URL provided');
}
branch = encodeURIComponentExceptSlashes(branch);
return `${hostPrefix}/${repo.owner}/${repo.repo}/tree/${branch}`;
}
export function getVscodeDevHost(): string {
return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`;
}
export async function ensurePublished(repository: Repository, file: vscode.Uri) {
await repository.status();
if ((repository.state.HEAD?.type === RefType.Head || repository.state.HEAD?.type === RefType.Tag)
// If HEAD is not published, make sure it is
&& !repository?.state.HEAD?.upstream
) {
const publishBranch = vscode.l10n.t('Publish Branch & Copy Link');
const selection = await vscode.window.showInformationMessage(
vscode.l10n.t('The current branch is not published to the remote. Would you like to publish your branch before copying a link?'),
{ modal: true },
publishBranch
);
if (selection !== publishBranch) {
throw new vscode.CancellationError();
}
await vscode.commands.executeCommand('git.publish');
}
const uncommittedChanges = [...repository.state.workingTreeChanges, ...repository.state.indexChanges];
if (uncommittedChanges.find((c) => c.uri.toString() === file.toString()) && !repository.state.HEAD?.ahead && !repository.state.HEAD?.behind) {
const commitChanges = vscode.l10n.t('Commit Changes');
const copyAnyway = vscode.l10n.t('Copy Anyway');
const selection = await vscode.window.showWarningMessage(
vscode.l10n.t('The current file has uncommitted changes. Please commit your changes before copying a link.'),
{ modal: true },
commitChanges,
copyAnyway
);
if (selection !== copyAnyway) {
// Focus the SCM view
vscode.commands.executeCommand('workbench.view.scm');
throw new vscode.CancellationError();
}
} else if (repository.state.HEAD?.ahead) {
const pushCommits = vscode.l10n.t('Push Commits & Copy Link');
const selection = await vscode.window.showInformationMessage(
vscode.l10n.t('The current branch has unpublished commits. Would you like to push your commits before copying a link?'),
{ modal: true },
pushCommits
);
if (selection !== pushCommits) {
throw new vscode.CancellationError();
}
await repository.push();
} else if (repository.state.HEAD?.behind) {
const pull = vscode.l10n.t('Pull Changes & Copy Link');
const selection = await vscode.window.showInformationMessage(
vscode.l10n.t('The current branch is not up to date. Would you like to pull before copying a link?'),
{ modal: true },
pull
);
if (selection !== pull) {
throw new vscode.CancellationError();
}
await repository.pull();
}
await repository.status();
}

View File

@ -1,224 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { API as GitAPI, Repository } from './typings/git';
import { getOctokit } from './auth';
import { TextEncoder } from 'util';
import { basename } from 'path';
import { Octokit } from '@octokit/rest';
import { isInCodespaces } from './pushErrorHandler';
function sanitizeRepositoryName(value: string): string {
return value.trim().replace(/[^a-z0-9_.]/ig, '-');
}
function getPick<T extends vscode.QuickPickItem>(quickpick: vscode.QuickPick<T>): Promise<T | undefined> {
return Promise.race<T | undefined>([
new Promise<T>(c => quickpick.onDidAccept(() => quickpick.selectedItems.length > 0 && c(quickpick.selectedItems[0]))),
new Promise<undefined>(c => quickpick.onDidHide(() => c(undefined)))
]);
}
export async function publishRepository(gitAPI: GitAPI, repository?: Repository): Promise<void> {
if (!vscode.workspace.workspaceFolders?.length) {
return;
}
let folder: vscode.Uri;
if (repository) {
folder = repository.rootUri;
} else if (gitAPI.repositories.length === 1) {
repository = gitAPI.repositories[0];
folder = repository.rootUri;
} else if (vscode.workspace.workspaceFolders.length === 1) {
folder = vscode.workspace.workspaceFolders[0].uri;
} else {
const picks = vscode.workspace.workspaceFolders.map(folder => ({ label: folder.name, folder }));
const placeHolder = vscode.l10n.t('Pick a folder to publish to GitHub');
const pick = await vscode.window.showQuickPick(picks, { placeHolder });
if (!pick) {
return;
}
folder = pick.folder.uri;
}
let quickpick = vscode.window.createQuickPick<vscode.QuickPickItem & { repo?: string; auth?: 'https' | 'ssh'; isPrivate?: boolean }>();
quickpick.ignoreFocusOut = true;
quickpick.placeholder = 'Repository Name';
quickpick.value = basename(folder.fsPath);
quickpick.show();
quickpick.busy = true;
let owner: string;
let octokit: Octokit;
try {
octokit = await getOctokit();
const user = await octokit.users.getAuthenticated({});
owner = user.data.login;
} catch (e) {
// User has cancelled sign in
quickpick.dispose();
return;
}
quickpick.busy = false;
let repo: string | undefined;
let isPrivate: boolean;
const onDidChangeValue = async () => {
const sanitizedRepo = sanitizeRepositoryName(quickpick.value);
if (!sanitizedRepo) {
quickpick.items = [];
} else {
quickpick.items = [
{ label: `$(repo) Publish to GitHub private repository`, description: `$(github) ${owner}/${sanitizedRepo}`, alwaysShow: true, repo: sanitizedRepo, isPrivate: true },
{ label: `$(repo) Publish to GitHub public repository`, description: `$(github) ${owner}/${sanitizedRepo}`, alwaysShow: true, repo: sanitizedRepo, isPrivate: false },
];
}
};
onDidChangeValue();
while (true) {
const listener = quickpick.onDidChangeValue(onDidChangeValue);
const pick = await getPick(quickpick);
listener.dispose();
repo = pick?.repo;
isPrivate = pick?.isPrivate ?? true;
if (repo) {
try {
quickpick.busy = true;
await octokit.repos.get({ owner, repo: repo });
quickpick.items = [{ label: `$(error) GitHub repository already exists`, description: `$(github) ${owner}/${repo}`, alwaysShow: true }];
} catch {
break;
} finally {
quickpick.busy = false;
}
}
}
quickpick.dispose();
if (!repo) {
return;
}
if (!repository) {
const gitignore = vscode.Uri.joinPath(folder, '.gitignore');
let shouldGenerateGitignore = false;
try {
await vscode.workspace.fs.stat(gitignore);
} catch (err) {
shouldGenerateGitignore = true;
}
if (shouldGenerateGitignore) {
quickpick = vscode.window.createQuickPick();
quickpick.placeholder = vscode.l10n.t('Select which files should be included in the repository.');
quickpick.canSelectMany = true;
quickpick.show();
try {
quickpick.busy = true;
const children = (await vscode.workspace.fs.readDirectory(folder))
.map(([name]) => name)
.filter(name => name !== '.git');
quickpick.items = children.map(name => ({ label: name }));
quickpick.selectedItems = quickpick.items;
quickpick.busy = false;
const result = await Promise.race([
new Promise<readonly vscode.QuickPickItem[]>(c => quickpick.onDidAccept(() => c(quickpick.selectedItems))),
new Promise<undefined>(c => quickpick.onDidHide(() => c(undefined)))
]);
if (!result || result.length === 0) {
return;
}
const ignored = new Set(children);
result.forEach(c => ignored.delete(c.label));
if (ignored.size > 0) {
const raw = [...ignored].map(i => `/${i}`).join('\n');
const encoder = new TextEncoder();
await vscode.workspace.fs.writeFile(gitignore, encoder.encode(raw));
}
} finally {
quickpick.dispose();
}
}
}
const githubRepository = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: false, title: 'Publish to GitHub' }, async progress => {
progress.report({
message: isPrivate
? vscode.l10n.t('Publishing to a private GitHub repository')
: vscode.l10n.t('Publishing to a public GitHub repository'),
increment: 25
});
type CreateRepositoryResponseData = Awaited<ReturnType<typeof octokit.repos.createForAuthenticatedUser>>['data'];
let createdGithubRepository: CreateRepositoryResponseData | undefined = undefined;
if (isInCodespaces()) {
createdGithubRepository = await vscode.commands.executeCommand<CreateRepositoryResponseData>('github.codespaces.publish', { name: repo!, isPrivate });
} else {
const res = await octokit.repos.createForAuthenticatedUser({
name: repo!,
private: isPrivate
});
createdGithubRepository = res.data;
}
if (createdGithubRepository) {
progress.report({ message: vscode.l10n.t('Creating first commit'), increment: 25 });
if (!repository) {
repository = await gitAPI.init(folder, { defaultBranch: createdGithubRepository.default_branch }) || undefined;
if (!repository) {
return;
}
await repository.commit('first commit', { all: true, postCommitCommand: null });
}
progress.report({ message: vscode.l10n.t('Uploading files'), increment: 25 });
const branch = await repository.getBranch('HEAD');
const protocol = vscode.workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol');
const remoteUrl = protocol === 'https' ? createdGithubRepository.clone_url : createdGithubRepository.ssh_url;
await repository.addRemote('origin', remoteUrl);
await repository.push('origin', branch.name, true);
}
return createdGithubRepository;
});
if (!githubRepository) {
return;
}
const openOnGitHub = vscode.l10n.t('Open on GitHub');
vscode.window.showInformationMessage(vscode.l10n.t('Successfully published the "{0}" repository to GitHub.', `${owner}/${repo}`), openOnGitHub).then(action => {
if (action === openOnGitHub) {
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(githubRepository.html_url));
}
});
}

View File

@ -1,324 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextDecoder } from 'util';
import { commands, env, ProgressLocation, Uri, window, workspace, QuickPickOptions, FileType, l10n, Disposable, TextDocumentContentProvider } from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import { getOctokit } from './auth';
import { GitErrorCodes, PushErrorHandler, Remote, Repository } from './typings/git';
import * as path from 'path';
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
export function isInCodespaces(): boolean {
return env.remoteName === 'codespaces';
}
const PR_TEMPLATE_FILES = [
{ dir: '.', files: ['pull_request_template.md', 'PULL_REQUEST_TEMPLATE.md'] },
{ dir: 'docs', files: ['pull_request_template.md', 'PULL_REQUEST_TEMPLATE.md'] },
{ dir: '.github', files: ['PULL_REQUEST_TEMPLATE.md', 'PULL_REQUEST_TEMPLATE.md'] }
];
const PR_TEMPLATE_DIRECTORY_NAMES = [
'PULL_REQUEST_TEMPLATE',
'docs/PULL_REQUEST_TEMPLATE',
'.github/PULL_REQUEST_TEMPLATE'
];
async function assertMarkdownFiles(dir: Uri, files: string[]): Promise<Uri[]> {
const dirFiles = await workspace.fs.readDirectory(dir);
return dirFiles
.filter(([name, type]) => Boolean(type & FileType.File) && files.indexOf(name) !== -1)
.map(([name]) => Uri.joinPath(dir, name));
}
async function findMarkdownFilesInDir(uri: Uri): Promise<Uri[]> {
const files = await workspace.fs.readDirectory(uri);
return files
.filter(([name, type]) => Boolean(type & FileType.File) && path.extname(name) === '.md')
.map(([name]) => Uri.joinPath(uri, name));
}
/**
* PR templates can be:
* - In the root, `docs`, or `.github` folders, called `pull_request_template.md` or `PULL_REQUEST_TEMPLATE.md`
* - Or, in a `PULL_REQUEST_TEMPLATE` directory directly below the root, `docs`, or `.github` folders, called `*.md`
*
* NOTE This method is a modified copy of a method with same name at microsoft/vscode-pull-request-github repository:
* https://github.com/microsoft/vscode-pull-request-github/blob/0a0c3c6c21c0b9c2f4d5ffbc3f8c6a825472e9e6/src/github/folderRepositoryManager.ts#L1061
*
*/
export async function findPullRequestTemplates(repositoryRootUri: Uri): Promise<Uri[]> {
const results = await Promise.allSettled([
...PR_TEMPLATE_FILES.map(x => assertMarkdownFiles(Uri.joinPath(repositoryRootUri, x.dir), x.files)),
...PR_TEMPLATE_DIRECTORY_NAMES.map(x => findMarkdownFilesInDir(Uri.joinPath(repositoryRootUri, x)))
]);
return results.flatMap(x => x.status === 'fulfilled' && x.value || []);
}
export async function pickPullRequestTemplate(repositoryRootUri: Uri, templates: Uri[]): Promise<Uri | undefined> {
const quickPickItemFromUri = (x: Uri) => ({ label: path.relative(repositoryRootUri.path, x.path), template: x });
const quickPickItems = [
{
label: l10n.t('No template'),
picked: true,
template: undefined,
},
...templates.map(quickPickItemFromUri)
];
const quickPickOptions: QuickPickOptions = {
placeHolder: l10n.t('Select the Pull Request template'),
ignoreFocusOut: true
};
const pickedTemplate = await window.showQuickPick(quickPickItems, quickPickOptions);
return pickedTemplate?.template;
}
class CommandErrorOutputTextDocumentContentProvider implements TextDocumentContentProvider {
private items = new Map<string, string>();
set(uri: Uri, contents: string): void {
this.items.set(uri.path, contents);
}
delete(uri: Uri): void {
this.items.delete(uri.path);
}
provideTextDocumentContent(uri: Uri): string | undefined {
return this.items.get(uri.path);
}
}
export class GithubPushErrorHandler implements PushErrorHandler {
private disposables: Disposable[] = [];
private commandErrors = new CommandErrorOutputTextDocumentContentProvider();
constructor(private readonly telemetryReporter: TelemetryReporter) {
this.disposables.push(workspace.registerTextDocumentContentProvider('github-output', this.commandErrors));
}
async handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { stderr: string; gitErrorCode: GitErrorCodes }): Promise<boolean> {
if (error.gitErrorCode !== GitErrorCodes.PermissionDenied && error.gitErrorCode !== GitErrorCodes.PushRejected) {
return false;
}
const remoteUrl = remote.pushUrl || (isInCodespaces() ? remote.fetchUrl : undefined);
if (!remoteUrl) {
return false;
}
const match = /^(?:https:\/\/github\.com\/|git@github\.com:)([^\/]+)\/([^\/.]+)/i.exec(remoteUrl);
if (!match) {
return false;
}
if (/^:/.test(refspec)) {
return false;
}
const [, owner, repo] = match;
if (error.gitErrorCode === GitErrorCodes.PermissionDenied) {
await this.handlePermissionDeniedError(repository, remote, refspec, owner, repo);
/* __GDPR__
"pushErrorHandler" : {
"owner": "lszomoru",
"handler": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryReporter.sendTelemetryEvent('pushErrorHandler', { handler: 'PermissionDenied' });
return true;
}
// Push protection
if (/GH009: Secrets detected!/i.test(error.stderr)) {
await this.handlePushProtectionError(owner, repo, error.stderr);
/* __GDPR__
"pushErrorHandler" : {
"owner": "lszomoru",
"handler": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryReporter.sendTelemetryEvent('pushErrorHandler', { handler: 'PushRejected.PushProtection' });
return true;
}
/* __GDPR__
"pushErrorHandler" : {
"owner": "lszomoru",
"handler": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryReporter.sendTelemetryEvent('pushErrorHandler', { handler: 'None' });
return false;
}
private async handlePermissionDeniedError(repository: Repository, remote: Remote, refspec: string, owner: string, repo: string): Promise<void> {
const yes = l10n.t('Create Fork');
const no = l10n.t('No');
const askFork = l10n.t('You don\'t have permissions to push to "{0}/{1}" on GitHub. Would you like to create a fork and push to it instead?', owner, repo);
const answer = await window.showWarningMessage(askFork, { modal: true }, yes, no);
if (answer !== yes) {
return;
}
const match = /^([^:]*):([^:]*)$/.exec(refspec);
const localName = match ? match[1] : refspec;
let remoteName = match ? match[2] : refspec;
const [octokit, ghRepository] = await window.withProgress({ location: ProgressLocation.Notification, cancellable: false, title: l10n.t('Create GitHub fork') }, async progress => {
progress.report({ message: l10n.t('Forking "{0}/{1}"...', owner, repo), increment: 33 });
const octokit = await getOctokit();
type CreateForkResponseData = Awaited<ReturnType<typeof octokit.repos.createFork>>['data'];
// Issue: what if the repo already exists?
let ghRepository: CreateForkResponseData;
try {
if (isInCodespaces()) {
// Call into the codespaces extension to fork the repository
const resp = await commands.executeCommand<{ repository: CreateForkResponseData; ref: string }>('github.codespaces.forkRepository');
if (!resp) {
throw new Error('Unable to fork respository');
}
ghRepository = resp.repository;
if (resp.ref) {
let ref = resp.ref;
if (ref.startsWith('refs/heads/')) {
ref = ref.substr(11);
}
remoteName = ref;
}
} else {
const resp = await octokit.repos.createFork({ owner, repo });
ghRepository = resp.data;
}
} catch (ex) {
console.error(ex);
throw ex;
}
progress.report({ message: l10n.t('Pushing changes...'), increment: 33 });
// Issue: what if there's already an `upstream` repo?
await repository.renameRemote(remote.name, 'upstream');
// Issue: what if there's already another `origin` repo?
const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol');
const remoteUrl = protocol === 'https' ? ghRepository.clone_url : ghRepository.ssh_url;
await repository.addRemote('origin', remoteUrl);
try {
await repository.fetch('origin', remoteName);
await repository.setBranchUpstream(localName, `origin/${remoteName}`);
} catch {
// noop
}
await repository.push('origin', localName, true);
return [octokit, ghRepository] as const;
});
// yield
(async () => {
const openOnGitHub = l10n.t('Open on GitHub');
const createPR = l10n.t('Create PR');
const action = await window.showInformationMessage(l10n.t('The fork "{0}" was successfully created on GitHub.', ghRepository.full_name), openOnGitHub, createPR);
if (action === openOnGitHub) {
await commands.executeCommand('vscode.open', Uri.parse(ghRepository.html_url));
} else if (action === createPR) {
const pr = await window.withProgress({ location: ProgressLocation.Notification, cancellable: false, title: l10n.t('Creating GitHub Pull Request...') }, async _ => {
let title = `Update ${remoteName}`;
const head = repository.state.HEAD?.name;
let body: string | undefined;
if (head) {
const commit = await repository.getCommit(head);
title = commit.message.split('\n')[0];
body = commit.message.slice(title.length + 1).trim();
}
const templates = await findPullRequestTemplates(repository.rootUri);
if (templates.length > 0) {
templates.sort((a, b) => a.path.localeCompare(b.path));
const template = await pickPullRequestTemplate(repository.rootUri, templates);
if (template) {
body = new TextDecoder('utf-8').decode(await workspace.fs.readFile(template));
}
}
const { data: pr } = await octokit.pulls.create({
owner,
repo,
title,
body,
head: `${ghRepository.owner.login}:${remoteName}`,
base: ghRepository.default_branch
});
await repository.setConfig(`branch.${localName}.remote`, 'upstream');
await repository.setConfig(`branch.${localName}.merge`, `refs/heads/${remoteName}`);
await repository.setConfig(`branch.${localName}.github-pr-owner-number`, `${owner}#${repo}#${pr.number}`);
return pr;
});
const openPR = l10n.t('Open PR');
const action = await window.showInformationMessage(l10n.t('The PR "{0}/{1}#{2}" was successfully created on GitHub.', owner, repo, pr.number), openPR);
if (action === openPR) {
await commands.executeCommand('vscode.open', Uri.parse(pr.html_url));
}
}
})();
}
private async handlePushProtectionError(owner: string, repo: string, stderr: string): Promise<void> {
// Open command output in an editor
const timestamp = new Date().getTime();
const uri = Uri.parse(`github-output:/github-error-${timestamp}`);
this.commandErrors.set(uri, stderr);
try {
const doc = await workspace.openTextDocument(uri);
await window.showTextDocument(doc);
}
finally {
this.commandErrors.set(uri, stderr);
}
// Show dialog
const learnMore = l10n.t('Learn More');
const message = l10n.t('Your push to "{0}/{1}" was rejected by GitHub because push protection is enabled and one or more secrets were detected.', owner, repo);
const answer = await window.showWarningMessage(message, { modal: true }, learnMore);
if (answer === learnMore) {
commands.executeCommand('vscode.open', 'https://aka.ms/vscode-github-push-protection');
}
}
dispose() {
this.disposables.forEach(d => d.dispose());
}
}

View File

@ -1,139 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri, env, l10n, workspace } from 'vscode';
import { RemoteSourceProvider, RemoteSource, RemoteSourceAction } from './typings/git-base';
import { getOctokit } from './auth';
import { Octokit } from '@octokit/rest';
import { getRepositoryFromQuery, getRepositoryFromUrl } from './util';
import { getBranchLink, getVscodeDevHost } from './links';
function asRemoteSource(raw: any): RemoteSource {
const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol');
return {
name: `$(github) ${raw.full_name}`,
description: `${raw.stargazers_count > 0 ? `$(star-full) ${raw.stargazers_count}` : ''
}`,
detail: raw.description || undefined,
url: protocol === 'https' ? raw.clone_url : raw.ssh_url
};
}
export class GithubRemoteSourceProvider implements RemoteSourceProvider {
readonly name = 'GitHub';
readonly icon = 'github';
readonly supportsQuery = true;
private userReposCache: RemoteSource[] = [];
async getRemoteSources(query?: string): Promise<RemoteSource[]> {
const octokit = await getOctokit();
if (query) {
const repository = getRepositoryFromUrl(query);
if (repository) {
const raw = await octokit.repos.get(repository);
return [asRemoteSource(raw.data)];
}
}
const all = await Promise.all([
this.getQueryRemoteSources(octokit, query),
this.getUserRemoteSources(octokit, query),
]);
const map = new Map<string, RemoteSource>();
for (const group of all) {
for (const remoteSource of group) {
map.set(remoteSource.name, remoteSource);
}
}
return [...map.values()];
}
private async getUserRemoteSources(octokit: Octokit, query?: string): Promise<RemoteSource[]> {
if (!query) {
const user = await octokit.users.getAuthenticated({});
const username = user.data.login;
const res = await octokit.repos.listForAuthenticatedUser({ username, sort: 'updated', per_page: 100 });
this.userReposCache = res.data.map(asRemoteSource);
}
return this.userReposCache;
}
private async getQueryRemoteSources(octokit: Octokit, query?: string): Promise<RemoteSource[]> {
if (!query) {
return [];
}
const repository = getRepositoryFromQuery(query);
if (repository) {
query = `user:${repository.owner}+${repository.repo}`;
}
query += ` fork:true`;
const raw = await octokit.search.repos({ q: query, sort: 'stars' });
return raw.data.items.map(asRemoteSource);
}
async getBranches(url: string): Promise<string[]> {
const repository = getRepositoryFromUrl(url);
if (!repository) {
return [];
}
const octokit = await getOctokit();
const branches: string[] = [];
let page = 1;
while (true) {
const res = await octokit.repos.listBranches({ ...repository, per_page: 100, page });
if (res.data.length === 0) {
break;
}
branches.push(...res.data.map(b => b.name));
page++;
}
const repo = await octokit.repos.get(repository);
const defaultBranch = repo.data.default_branch;
return branches.sort((a, b) => a === defaultBranch ? -1 : b === defaultBranch ? 1 : 0);
}
async getRemoteSourceActions(url: string): Promise<RemoteSourceAction[]> {
const repository = getRepositoryFromUrl(url);
if (!repository) {
return [];
}
return [{
label: l10n.t('Open on GitHub'),
icon: 'github',
run(branch: string) {
const link = getBranchLink(url, branch);
env.openExternal(Uri.parse(link));
}
}, {
label: l10n.t('Checkout on vscode.dev'),
icon: 'globe',
run(branch: string) {
const link = getBranchLink(url, branch, getVscodeDevHost());
env.openExternal(Uri.parse(link));
}
}];
}
}

View File

@ -1,18 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { publishRepository } from './publish';
import { API as GitAPI, RemoteSourcePublisher, Repository } from './typings/git';
export class GithubRemoteSourcePublisher implements RemoteSourcePublisher {
readonly name = 'GitHub';
readonly icon = 'github';
constructor(private gitAPI: GitAPI) { }
publishRepository(repository: Repository): Promise<void> {
return publishRepository(this.gitAPI, repository);
}
}

View File

@ -1,113 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { API } from './typings/git';
import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util';
import { encodeURIComponentExceptSlashes, ensurePublished, getRepositoryForFile, notebookCellRangeString, rangeString } from './links';
export class VscodeDevShareProvider implements vscode.ShareProvider, vscode.Disposable {
readonly id: string = 'copyVscodeDevLink';
readonly label: string = vscode.l10n.t('Copy vscode.dev Link');
readonly priority: number = 10;
private _hasGitHubRepositories: boolean = false;
private set hasGitHubRepositories(value: boolean) {
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', value);
this._hasGitHubRepositories = value;
this.ensureShareProviderRegistration();
}
private shareProviderRegistration: vscode.Disposable | undefined;
private disposables: vscode.Disposable[] = [];
constructor(private readonly gitAPI: API) {
this.initializeGitHubRepoContext();
}
dispose() {
this.disposables.forEach(d => d.dispose());
}
private initializeGitHubRepoContext() {
if (this.gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
this.hasGitHubRepositories = true;
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
} else {
this.disposables.push(this.gitAPI.onDidOpenRepository(async e => {
await e.status();
if (repositoryHasGitHubRemote(e)) {
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
this.hasGitHubRepositories = true;
}
}));
}
this.disposables.push(this.gitAPI.onDidCloseRepository(() => {
if (!this.gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
this.hasGitHubRepositories = false;
}
}));
}
private ensureShareProviderRegistration() {
if (vscode.env.appHost !== 'codespaces' && !this.shareProviderRegistration && this._hasGitHubRepositories) {
const shareProviderRegistration = vscode.window.registerShareProvider({ scheme: 'file' }, this);
this.shareProviderRegistration = shareProviderRegistration;
this.disposables.push(shareProviderRegistration);
} else if (this.shareProviderRegistration && !this._hasGitHubRepositories) {
this.shareProviderRegistration.dispose();
this.shareProviderRegistration = undefined;
}
}
async provideShare(item: vscode.ShareableItem, _token: vscode.CancellationToken): Promise<vscode.Uri | undefined> {
const repository = getRepositoryForFile(this.gitAPI, item.resourceUri);
if (!repository) {
return;
}
await ensurePublished(repository, item.resourceUri);
let repo: { owner: string; repo: string } | undefined;
repository.state.remotes.find(remote => {
if (remote.fetchUrl) {
const foundRepo = getRepositoryFromUrl(remote.fetchUrl);
if (foundRepo && (remote.name === repository.state.HEAD?.upstream?.remote)) {
repo = foundRepo;
return;
} else if (foundRepo && !repo) {
repo = foundRepo;
}
}
return;
});
if (!repo) {
return;
}
const blobSegment = repository?.state.HEAD?.name ? encodeURIComponentExceptSlashes(repository.state.HEAD?.name) : repository?.state.HEAD?.commit;
const filepathSegment = encodeURIComponentExceptSlashes(item.resourceUri.path.substring(repository?.rootUri.path.length));
const rangeSegment = getRangeSegment(item);
return vscode.Uri.parse(`${this.getVscodeDevHost()}/${repo.owner}/${repo.repo}/blob/${blobSegment}${filepathSegment}${rangeSegment}`);
}
private getVscodeDevHost(): string {
return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`;
}
}
function getRangeSegment(item: vscode.ShareableItem) {
if (item.resourceUri.scheme === 'vscode-notebook-cell') {
const notebookEditor = vscode.window.visibleNotebookEditors.find(editor => editor.notebook.uri.fsPath === item.resourceUri.fsPath);
const cell = notebookEditor?.notebook.getCells().find(cell => cell.document.uri.fragment === item.resourceUri?.fragment);
const cellIndex = cell?.index ?? notebookEditor?.selection.start;
return notebookCellRangeString(cellIndex, item.selection);
}
return rangeString(item.selection);
}

View File

@ -1,65 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { workspace, extensions, Uri, commands } from 'vscode';
import { findPullRequestTemplates, pickPullRequestTemplate } from '../pushErrorHandler';
suite('github smoke test', function () {
const cwd = workspace.workspaceFolders![0].uri;
suiteSetup(async function () {
const ext = extensions.getExtension('vscode.github');
await ext?.activate();
});
test('should find all templates', async function () {
const expectedValuesSorted = [
'PULL_REQUEST_TEMPLATE/a.md',
'PULL_REQUEST_TEMPLATE/b.md',
'docs/PULL_REQUEST_TEMPLATE.md',
'docs/PULL_REQUEST_TEMPLATE/a.md',
'docs/PULL_REQUEST_TEMPLATE/b.md',
'.github/PULL_REQUEST_TEMPLATE.md',
'.github/PULL_REQUEST_TEMPLATE/a.md',
'.github/PULL_REQUEST_TEMPLATE/b.md',
'PULL_REQUEST_TEMPLATE.md'
];
expectedValuesSorted.sort();
const uris = await findPullRequestTemplates(cwd);
const urisSorted = uris.map(x => x.path.slice(cwd.path.length));
urisSorted.sort();
assert.deepStrictEqual(urisSorted, expectedValuesSorted);
});
test('selecting non-default quick-pick item should correspond to a template', async () => {
const template0 = Uri.file("some-imaginary-template-0");
const template1 = Uri.file("some-imaginary-template-1");
const templates = [template0, template1];
const pick = pickPullRequestTemplate(Uri.file("/"), templates);
await commands.executeCommand('workbench.action.quickOpenSelectNext');
await commands.executeCommand('workbench.action.quickOpenSelectNext');
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
assert.ok(await pick === template0);
});
test('selecting first quick-pick item should return undefined', async () => {
const templates = [Uri.file("some-imaginary-file")];
const pick = pickPullRequestTemplate(Uri.file("/"), templates);
await commands.executeCommand('workbench.action.quickOpenSelectNext');
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
assert.ok(await pick === undefined);
});
});

View File

@ -1,30 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as testRunner from '../../../../test/integration/electron/testrunner';
const suite = 'Github Tests';
const options: import('mocha').MochaOptions = {
ui: 'tdd',
color: true,
timeout: 60000
};
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
options.reporter = 'mocha-multi-reporters';
options.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} ${process.platform}`,
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}
testRunner.configure(options);
export = testRunner;

View File

@ -1,85 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event, ProviderResult, Uri } from 'vscode';
export { ProviderResult } from 'vscode';
export interface API {
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
pickRemoteSource(options: PickRemoteSourceOptions): Promise<string | PickRemoteSourceResult | undefined>;
}
export interface GitBaseExtension {
readonly enabled: boolean;
readonly onDidChangeEnablement: Event<boolean>;
/**
* Returns a specific API version.
*
* Throws error if git-base extension is disabled. You can listed to the
* [GitBaseExtension.onDidChangeEnablement](#GitBaseExtension.onDidChangeEnablement)
* event to know when the extension becomes enabled/disabled.
*
* @param version Version number.
* @returns API instance
*/
getAPI(version: 1): API;
}
export interface PickRemoteSourceOptions {
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
readonly urlLabel?: string | ((url: string) => string);
readonly providerName?: string;
readonly title?: string;
readonly placeholder?: string;
readonly branch?: boolean; // then result is PickRemoteSourceResult
readonly showRecentSources?: boolean;
}
export interface PickRemoteSourceResult {
readonly url: string;
readonly branch?: string;
}
export interface RemoteSourceAction {
readonly label: string;
/**
* Codicon name
*/
readonly icon: string;
run(branch: string): void;
}
export interface RemoteSource {
readonly name: string;
readonly description?: string;
readonly detail?: string;
/**
* Codicon name
*/
readonly icon?: string;
readonly url: string | string[];
}
export interface RecentRemoteSource extends RemoteSource {
readonly timestamp: number;
}
export interface RemoteSourceProvider {
readonly name: string;
/**
* Codicon name
*/
readonly icon?: string;
readonly label?: string;
readonly placeholder?: string;
readonly supportsQuery?: boolean;
getBranches?(url: string): ProviderResult<string[]>;
getRemoteSourceActions?(url: string): ProviderResult<RemoteSourceAction[]>;
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}

View File

@ -1,376 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri, Event, Disposable, ProviderResult, Command } from 'vscode';
export { ProviderResult } from 'vscode';
export interface Git {
readonly path: string;
}
export interface InputBox {
value: string;
}
export const enum ForcePushMode {
Force,
ForceWithLease
}
export const enum RefType {
Head,
RemoteHead,
Tag
}
export interface Ref {
readonly type: RefType;
readonly name?: string;
readonly commit?: string;
readonly remote?: string;
}
export interface UpstreamRef {
readonly remote: string;
readonly name: string;
}
export interface Branch extends Ref {
readonly upstream?: UpstreamRef;
readonly ahead?: number;
readonly behind?: number;
}
export interface Commit {
readonly hash: string;
readonly message: string;
readonly parents: string[];
readonly authorDate?: Date;
readonly authorName?: string;
readonly authorEmail?: string;
readonly commitDate?: Date;
}
export interface Submodule {
readonly name: string;
readonly path: string;
readonly url: string;
}
export interface Remote {
readonly name: string;
readonly fetchUrl?: string;
readonly pushUrl?: string;
readonly isReadOnly: boolean;
}
export const enum Status {
INDEX_MODIFIED,
INDEX_ADDED,
INDEX_DELETED,
INDEX_RENAMED,
INDEX_COPIED,
MODIFIED,
DELETED,
UNTRACKED,
IGNORED,
INTENT_TO_ADD,
INTENT_TO_RENAME,
TYPE_CHANGED,
ADDED_BY_US,
ADDED_BY_THEM,
DELETED_BY_US,
DELETED_BY_THEM,
BOTH_ADDED,
BOTH_DELETED,
BOTH_MODIFIED
}
export interface Change {
/**
* Returns either `originalUri` or `renameUri`, depending
* on whether this change is a rename change. When
* in doubt always use `uri` over the other two alternatives.
*/
readonly uri: Uri;
readonly originalUri: Uri;
readonly renameUri: Uri | undefined;
readonly status: Status;
}
export interface RepositoryState {
readonly HEAD: Branch | undefined;
readonly refs: Ref[];
readonly remotes: Remote[];
readonly submodules: Submodule[];
readonly rebaseCommit: Commit | undefined;
readonly mergeChanges: Change[];
readonly indexChanges: Change[];
readonly workingTreeChanges: Change[];
readonly onDidChange: Event<void>;
}
export interface RepositoryUIState {
readonly selected: boolean;
readonly onDidChange: Event<void>;
}
/**
* Log options.
*/
export interface LogOptions {
/** Max number of log entries to retrieve. If not specified, the default is 32. */
readonly maxEntries?: number;
readonly path?: string;
}
export interface CommitOptions {
all?: boolean | 'tracked';
amend?: boolean;
signoff?: boolean;
signCommit?: boolean;
empty?: boolean;
noVerify?: boolean;
requireUserConfig?: boolean;
useEditor?: boolean;
verbose?: boolean;
/**
* string - execute the specified command after the commit operation
* undefined - execute the command specified in git.postCommitCommand
* after the commit operation
* null - do not execute any command after the commit operation
*/
postCommitCommand?: string | null;
}
export interface FetchOptions {
remote?: string;
ref?: string;
all?: boolean;
prune?: boolean;
depth?: number;
}
export interface InitOptions {
defaultBranch?: string;
}
export interface BranchQuery {
readonly remote?: boolean;
readonly pattern?: string;
readonly count?: number;
readonly contains?: string;
}
export interface Repository {
readonly rootUri: Uri;
readonly inputBox: InputBox;
readonly state: RepositoryState;
readonly ui: RepositoryUIState;
getConfigs(): Promise<{ key: string; value: string; }[]>;
getConfig(key: string): Promise<string>;
setConfig(key: string, value: string): Promise<string>;
getGlobalConfig(key: string): Promise<string>;
getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>;
detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }>;
buffer(ref: string, path: string): Promise<Buffer>;
show(ref: string, path: string): Promise<string>;
getCommit(ref: string): Promise<Commit>;
add(paths: string[]): Promise<void>;
revert(paths: string[]): Promise<void>;
clean(paths: string[]): Promise<void>;
apply(patch: string, reverse?: boolean): Promise<void>;
diff(cached?: boolean): Promise<string>;
diffWithHEAD(): Promise<Change[]>;
diffWithHEAD(path: string): Promise<string>;
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffIndexWithHEAD(): Promise<Change[]>;
diffIndexWithHEAD(path: string): Promise<string>;
diffIndexWith(ref: string): Promise<Change[]>;
diffIndexWith(ref: string, path: string): Promise<string>;
diffBlobs(object1: string, object2: string): Promise<string>;
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
hashObject(data: string): Promise<string>;
createBranch(name: string, checkout: boolean, ref?: string): Promise<void>;
deleteBranch(name: string, force?: boolean): Promise<void>;
getBranch(name: string): Promise<Branch>;
getBranches(query: BranchQuery): Promise<Ref[]>;
setBranchUpstream(name: string, upstream: string): Promise<void>;
getMergeBase(ref1: string, ref2: string): Promise<string>;
tag(name: string, upstream: string): Promise<void>;
deleteTag(name: string): Promise<void>;
status(): Promise<void>;
checkout(treeish: string): Promise<void>;
addRemote(name: string, url: string): Promise<void>;
removeRemote(name: string): Promise<void>;
renameRemote(name: string, newName: string): Promise<void>;
fetch(options?: FetchOptions): Promise<void>;
fetch(remote?: string, ref?: string, depth?: number): Promise<void>;
pull(unshallow?: boolean): Promise<void>;
push(remoteName?: string, branchName?: string, setUpstream?: boolean, force?: ForcePushMode): Promise<void>;
blame(path: string): Promise<string>;
log(options?: LogOptions): Promise<Commit[]>;
commit(message: string, opts?: CommitOptions): Promise<void>;
}
export interface RemoteSource {
readonly name: string;
readonly description?: string;
readonly url: string | string[];
}
export interface RemoteSourceProvider {
readonly name: string;
readonly icon?: string; // codicon name
readonly supportsQuery?: boolean;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
getBranches?(url: string): ProviderResult<string[]>;
publishRepository?(repository: Repository): Promise<void>;
}
export interface RemoteSourcePublisher {
readonly name: string;
readonly icon?: string; // codicon name
publishRepository(repository: Repository): Promise<void>;
}
export interface Credentials {
readonly username: string;
readonly password: string;
}
export interface CredentialsProvider {
getCredentials(host: Uri): ProviderResult<Credentials>;
}
export interface PostCommitCommandsProvider {
getCommands(repository: Repository): Command[];
}
export interface PushErrorHandler {
handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean>;
}
export interface BranchProtection {
readonly remote: string;
readonly rules: BranchProtectionRule[];
}
export interface BranchProtectionRule {
readonly include?: string[];
readonly exclude?: string[];
}
export interface BranchProtectionProvider {
onDidChangeBranchProtection: Event<Uri>;
provideBranchProtection(): BranchProtection[];
}
export type APIState = 'uninitialized' | 'initialized';
export interface PublishEvent {
repository: Repository;
branch?: string;
}
export interface API {
readonly state: APIState;
readonly onDidChangeState: Event<APIState>;
readonly onDidPublish: Event<PublishEvent>;
readonly git: Git;
readonly repositories: Repository[];
readonly onDidOpenRepository: Event<Repository>;
readonly onDidCloseRepository: Event<Repository>;
toGitUri(uri: Uri, ref: string): Uri;
getRepository(uri: Uri): Repository | null;
init(root: Uri, options?: InitOptions): Promise<Repository | null>;
openRepository(root: Uri): Promise<Repository | null>
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable;
}
export interface GitExtension {
readonly enabled: boolean;
readonly onDidChangeEnablement: Event<boolean>;
/**
* Returns a specific API version.
*
* Throws error if git extension is disabled. You can listen to the
* [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event
* to know when the extension becomes enabled/disabled.
*
* @param version Version number.
* @returns API instance
*/
getAPI(version: 1): API;
}
export const enum GitErrorCodes {
BadConfigFile = 'BadConfigFile',
AuthenticationFailed = 'AuthenticationFailed',
NoUserNameConfigured = 'NoUserNameConfigured',
NoUserEmailConfigured = 'NoUserEmailConfigured',
NoRemoteRepositorySpecified = 'NoRemoteRepositorySpecified',
NotAGitRepository = 'NotAGitRepository',
NotAtRepositoryRoot = 'NotAtRepositoryRoot',
Conflict = 'Conflict',
StashConflict = 'StashConflict',
UnmergedChanges = 'UnmergedChanges',
PushRejected = 'PushRejected',
RemoteConnectionError = 'RemoteConnectionError',
DirtyWorkTree = 'DirtyWorkTree',
CantOpenResource = 'CantOpenResource',
GitNotFound = 'GitNotFound',
CantCreatePipe = 'CantCreatePipe',
PermissionDenied = 'PermissionDenied',
CantAccessRemote = 'CantAccessRemote',
RepositoryNotFound = 'RepositoryNotFound',
RepositoryIsLocked = 'RepositoryIsLocked',
BranchNotFullyMerged = 'BranchNotFullyMerged',
NoRemoteReference = 'NoRemoteReference',
InvalidBranchName = 'InvalidBranchName',
BranchAlreadyExists = 'BranchAlreadyExists',
NoLocalChanges = 'NoLocalChanges',
NoStashFound = 'NoStashFound',
LocalChangesOverwritten = 'LocalChangesOverwritten',
NoUpstreamBranch = 'NoUpstreamBranch',
IsInSubmodule = 'IsInSubmodule',
WrongCase = 'WrongCase',
CantLockRef = 'CantLockRef',
CantRebaseMultipleBranches = 'CantRebaseMultipleBranches',
PatchDoesNotApply = 'PatchDoesNotApply',
NoPathFound = 'NoPathFound',
UnknownPath = 'UnknownPath',
EmptyCommitMessage = 'EmptyCommitMessage'
}

View File

@ -1,6 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'tunnel';

View File

@ -1,47 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
// https://github.com/microsoft/vscode/issues/180582
export namespace workspace {
/**
*
* @param scheme The URI scheme that this provider can provide canonical URIs for.
* A canonical URI represents the conversion of a resource's alias into a source of truth URI.
* Multiple aliases may convert to the same source of truth URI.
* @param provider A provider which can convert URIs of scheme @param scheme to
* a canonical URI which is stable across machines.
*/
export function registerCanonicalUriProvider(scheme: string, provider: CanonicalUriProvider): Disposable;
/**
*
* @param uri The URI to provide a canonical URI for.
* @param token A cancellation token for the request.
*/
export function getCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult<Uri>;
}
export interface CanonicalUriProvider {
/**
*
* @param uri The URI to provide a canonical URI for.
* @param options Options that the provider should honor in the URI it returns.
* @param token A cancellation token for the request.
* @returns The canonical URI for the requested URI or undefined if no canonical URI can be provided.
*/
provideCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult<Uri>;
}
export interface CanonicalUriRequestOptions {
/**
*
* The desired scheme of the canonical URI.
*/
targetScheme: string;
}
}

View File

@ -1,29 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// https://github.com/microsoft/vscode/issues/176316
declare module 'vscode' {
export interface TreeItem {
shareableItem?: ShareableItem;
}
export interface ShareableItem {
resourceUri: Uri;
selection?: Range;
}
export interface ShareProvider {
readonly id: string;
readonly label: string;
readonly priority: number;
provideShare(item: ShareableItem, token: CancellationToken): ProviderResult<Uri>;
}
export namespace window {
export function registerShareProvider(selector: DocumentSelector, provider: ShareProvider): Disposable;
}
}

View File

@ -1,39 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { Repository } from './typings/git';
export class DisposableStore {
private disposables = new Set<vscode.Disposable>();
add(disposable: vscode.Disposable): void {
this.disposables.add(disposable);
}
dispose(): void {
for (const disposable of this.disposables) {
disposable.dispose();
}
this.disposables.clear();
}
}
export function getRepositoryFromUrl(url: string): { owner: string; repo: string } | undefined {
const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(\.git)?$/i.exec(url)
|| /^git@github\.com:([^/]+)\/([^/]+?)(\.git)?$/i.exec(url);
return match ? { owner: match[1], repo: match[2] } : undefined;
}
export function getRepositoryFromQuery(query: string): { owner: string; repo: string } | undefined {
const match = /^([^/]+)\/([^/]+)$/i.exec(query);
return match ? { owner: match[1], repo: match[2] } : undefined;
}
export function repositoryHasGitHubRemote(repository: Repository) {
return !!repository.state.remotes.find(remote => remote.fetchUrl ? getRepositoryFromUrl(remote.fetchUrl) : undefined);
}

View File

@ -1,14 +0,0 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out",
"experimentalDecorators": true,
"typeRoots": [
"./node_modules/@types"
]
},
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts"
]
}

View File

@ -1,319 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@microsoft/1ds-core-js@4.0.3", "@microsoft/1ds-core-js@^4.0.3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz#c8a92c623745a9595e06558a866658480c33bdf9"
integrity sha512-FrxNLVAPsAvD7+l63TlNS/Kodvpct2WulpDSn1dI4Xuy0kF4E2H867kHdwL/iY1Bj3zA3FSy/jvE4+OcDws7ug==
dependencies:
"@microsoft/applicationinsights-core-js" "3.0.4"
"@microsoft/applicationinsights-shims" "3.0.1"
"@microsoft/dynamicproto-js" "^2.0.2"
"@nevware21/ts-async" ">= 0.3.0 < 2.x"
"@nevware21/ts-utils" ">= 0.10.1 < 2.x"
"@microsoft/1ds-post-js@^4.0.3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-4.0.3.tgz#cfcb20bb23fb6215d3f0732f60f5b7df3e624f86"
integrity sha512-uewvmUtXKd7ttypiKQGdYI6i7UUpPkOznLayzIFrJ4r2xnG6jhPjpKRncHFXPQcM4XSWO3yf5PQ3xAbPq9t7ZQ==
dependencies:
"@microsoft/1ds-core-js" "4.0.3"
"@microsoft/applicationinsights-shims" "3.0.1"
"@microsoft/dynamicproto-js" "^2.0.2"
"@nevware21/ts-async" ">= 0.3.0 < 2.x"
"@nevware21/ts-utils" ">= 0.10.1 < 2.x"
"@microsoft/applicationinsights-channel-js@3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.4.tgz#247b6fe2158fad9826cbcdf7304f885766b36624"
integrity sha512-6TlfExmErQ8Y+/ChbkyWl+jyt4wg3T6p7lwXDsUCB0LgZmlEWMaCUS0YlT73JCWmE8j7vxW8yUm0lgsgmHns3A==
dependencies:
"@microsoft/applicationinsights-common" "3.0.4"
"@microsoft/applicationinsights-core-js" "3.0.4"
"@microsoft/applicationinsights-shims" "3.0.1"
"@microsoft/dynamicproto-js" "^2.0.2"
"@nevware21/ts-async" ">= 0.3.0 < 2.x"
"@nevware21/ts-utils" ">= 0.10.1 < 2.x"
"@microsoft/applicationinsights-common@3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.4.tgz#c4aa53ba343f5b3c7fbf54cddd3c86a5bdcd95dc"
integrity sha512-r5gWaw/K9+tKfuo2GtDiDiKASgOkPOCrKW+wZzFvuR06uuwvWjbVQ6yW/YbnfuhRF5M65ksUiMi0eCMwEOGq7Q==
dependencies:
"@microsoft/applicationinsights-core-js" "3.0.4"
"@microsoft/applicationinsights-shims" "3.0.1"
"@microsoft/dynamicproto-js" "^2.0.2"
"@nevware21/ts-utils" ">= 0.10.1 < 2.x"
"@microsoft/applicationinsights-core-js@3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.4.tgz#008308b786930d94a1de8a1fbb4af0351b74653e"
integrity sha512-anxy5kEkqBmVoEqJiJzaaXXA0wzqZi9U4zGd05xFJ04lWckP8dG3zyT3+GGdg7rDelqLTNGxndeYoFmDv63u1g==
dependencies:
"@microsoft/applicationinsights-shims" "3.0.1"
"@microsoft/dynamicproto-js" "^2.0.2"
"@nevware21/ts-async" ">= 0.3.0 < 2.x"
"@nevware21/ts-utils" ">= 0.10.1 < 2.x"
"@microsoft/applicationinsights-shims@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz#3865b73ace8405b9c4618cc5c571f2fe3876f06f"
integrity sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==
dependencies:
"@nevware21/ts-utils" ">= 0.9.4 < 2.x"
"@microsoft/applicationinsights-web-basic@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.4.tgz#9a23323276b4a5a0dc6a352e2de5d75e3c16b534"
integrity sha512-KfoxPlLlf0JT12ADb23C5iGye/yFouoMgHEKULxkSQcYY9SsW/8rVrqqvoYKAL+u215CZU2A8Kc8sR3ehEaPCQ==
dependencies:
"@microsoft/applicationinsights-channel-js" "3.0.4"
"@microsoft/applicationinsights-common" "3.0.4"
"@microsoft/applicationinsights-core-js" "3.0.4"
"@microsoft/applicationinsights-shims" "3.0.1"
"@microsoft/dynamicproto-js" "^2.0.2"
"@nevware21/ts-async" ">= 0.3.0 < 2.x"
"@nevware21/ts-utils" ">= 0.10.1 < 2.x"
"@microsoft/dynamicproto-js@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz#e57fbec2e7067d48b7e8e1e1c1d354028ef718a6"
integrity sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==
dependencies:
"@nevware21/ts-utils" ">= 0.9.4 < 2.x"
"@nevware21/ts-async@>= 0.3.0 < 2.x":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@nevware21/ts-async/-/ts-async-0.3.0.tgz#a8b97ba01065fc930de9a3f4dd4a05e862becc6c"
integrity sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==
dependencies:
"@nevware21/ts-utils" ">= 0.10.0 < 2.x"
"@nevware21/ts-utils@>= 0.10.0 < 2.x", "@nevware21/ts-utils@>= 0.10.1 < 2.x", "@nevware21/ts-utils@>= 0.9.4 < 2.x":
version "0.10.1"
resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7"
integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==
"@octokit/auth-token@^3.0.0":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-3.0.1.tgz#88bc2baf5d706cb258474e722a720a8365dff2ec"
integrity sha512-/USkK4cioY209wXRpund6HZzHo9GmjakpV9ycOkpMcMxMk7QVcVFVyCMtzvXYiHsB2crgDgrtNYSELYFBXhhaA==
dependencies:
"@octokit/types" "^7.0.0"
"@octokit/core@^4.0.0":
version "4.0.5"
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-4.0.5.tgz#589e68c0a35d2afdcd41dafceab072c2fbc6ab5f"
integrity sha512-4R3HeHTYVHCfzSAi0C6pbGXV8UDI5Rk+k3G7kLVNckswN9mvpOzW9oENfjfH3nEmzg8y3AmKmzs8Sg6pLCeOCA==
dependencies:
"@octokit/auth-token" "^3.0.0"
"@octokit/graphql" "^5.0.0"
"@octokit/request" "^6.0.0"
"@octokit/request-error" "^3.0.0"
"@octokit/types" "^7.0.0"
before-after-hook "^2.2.0"
universal-user-agent "^6.0.0"
"@octokit/endpoint@^7.0.0":
version "7.0.1"
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-7.0.1.tgz#cb0d03e62e8762f3c80e52b025179de81899a823"
integrity sha512-/wTXAJwt0HzJ2IeE4kQXO+mBScfzyCkI0hMtkIaqyXd9zg76OpOfNQfHL9FlaxAV2RsNiOXZibVWloy8EexENg==
dependencies:
"@octokit/types" "^7.0.0"
is-plain-object "^5.0.0"
universal-user-agent "^6.0.0"
"@octokit/graphql-schema@14.4.0":
version "14.4.0"
resolved "https://registry.yarnpkg.com/@octokit/graphql-schema/-/graphql-schema-14.4.0.tgz#9336f64c3103a2e82ee3ce060c3ccf99d177d7f0"
integrity sha512-+O6/dsLlR6V9gv+t1lqsN+x73TLwyQWZpd3M8/eYnuny7VaznV9TAyUxf18tX8WBBS5IqtlLDk1nG+aSTPRZzQ==
dependencies:
graphql "^16.0.0"
graphql-tag "^2.10.3"
"@octokit/graphql@5.0.5":
version "5.0.5"
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-5.0.5.tgz#a4cb3ea73f83b861893a6370ee82abb36e81afd2"
integrity sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==
dependencies:
"@octokit/request" "^6.0.0"
"@octokit/types" "^9.0.0"
universal-user-agent "^6.0.0"
"@octokit/graphql@^5.0.0":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-5.0.1.tgz#a06982514ad131fb6fbb9da968653b2233fade9b"
integrity sha512-sxmnewSwAixkP1TrLdE6yRG53eEhHhDTYUykUwdV9x8f91WcbhunIHk9x1PZLALdBZKRPUO2HRcm4kezZ79HoA==
dependencies:
"@octokit/request" "^6.0.0"
"@octokit/types" "^7.0.0"
universal-user-agent "^6.0.0"
"@octokit/openapi-types@^13.6.0":
version "13.6.0"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-13.6.0.tgz#381884008e23fd82fd444553f6b4dcd24a5c4a4d"
integrity sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==
"@octokit/openapi-types@^17.1.0":
version "17.1.0"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-17.1.0.tgz#9a712b5bb9d644940d8a1f24115c798c317a64a5"
integrity sha512-rnI26BAITDZTo5vqFOmA7oX4xRd18rO+gcK4MiTpJmsRMxAw0JmevNjPsjpry1bb9SVNo56P/0kbiyXXa4QluA==
"@octokit/plugin-paginate-rest@^4.0.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-4.2.0.tgz#41fc6ca312446a85a4275aca698b4d9c4c5e06ab"
integrity sha512-8otLCIK9esfmOCY14CBnG/xPqv0paf14rc+s9tHpbOpeFwrv5CnECKW1qdqMAT60ngAa9eB1bKQ+l2YCpi0HPQ==
dependencies:
"@octokit/types" "^7.2.0"
"@octokit/plugin-request-log@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85"
integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==
"@octokit/plugin-rest-endpoint-methods@^6.0.0":
version "6.4.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.4.0.tgz#09584dd4e85fc4fe04ade45620b105af582c20ba"
integrity sha512-YP4eUqZ6vORy/eZOTdil1ZSrMt0kv7i/CVw+HhC2C0yJN+IqTc/rot957JQ7JfyeJD6HZOjLg6Jp1o9cPhI9KA==
dependencies:
"@octokit/types" "^7.2.0"
deprecation "^2.3.1"
"@octokit/request-error@^3.0.0":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-3.0.1.tgz#3fd747913c06ab2195e52004a521889dadb4b295"
integrity sha512-ym4Bp0HTP7F3VFssV88WD1ZyCIRoE8H35pXSKwLeMizcdZAYc/t6N9X9Yr9n6t3aG9IH75XDnZ6UeZph0vHMWQ==
dependencies:
"@octokit/types" "^7.0.0"
deprecation "^2.0.0"
once "^1.4.0"
"@octokit/request@^6.0.0":
version "6.2.1"
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-6.2.1.tgz#3ceeb22dab09a29595d96594b6720fc14495cf4e"
integrity sha512-gYKRCia3cpajRzDSU+3pt1q2OcuC6PK8PmFIyxZDWCzRXRSIBH8jXjFJ8ZceoygBIm0KsEUg4x1+XcYBz7dHPQ==
dependencies:
"@octokit/endpoint" "^7.0.0"
"@octokit/request-error" "^3.0.0"
"@octokit/types" "^7.0.0"
is-plain-object "^5.0.0"
node-fetch "^2.6.7"
universal-user-agent "^6.0.0"
"@octokit/rest@19.0.4":
version "19.0.4"
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-19.0.4.tgz#fd8bed1cefffa486e9ae46a9dc608ce81bcfcbdd"
integrity sha512-LwG668+6lE8zlSYOfwPj4FxWdv/qFXYBpv79TWIQEpBLKA9D/IMcWsF/U9RGpA3YqMVDiTxpgVpEW3zTFfPFTA==
dependencies:
"@octokit/core" "^4.0.0"
"@octokit/plugin-paginate-rest" "^4.0.0"
"@octokit/plugin-request-log" "^1.0.4"
"@octokit/plugin-rest-endpoint-methods" "^6.0.0"
"@octokit/types@^7.0.0", "@octokit/types@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-7.2.0.tgz#7ee0fc27f9f463d7ccf12ca5956988d498b3c6c4"
integrity sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==
dependencies:
"@octokit/openapi-types" "^13.6.0"
"@octokit/types@^9.0.0":
version "9.2.0"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-9.2.0.tgz#0358e3de070b1d43c5a8af63b9951c88a09fc9ed"
integrity sha512-xySzJG4noWrIBFyMu4lg4tu9vAgNg9S0aoLRONhAEz6ueyi1evBzb40HitIosaYS4XOexphG305IVcLrIX/30g==
dependencies:
"@octokit/openapi-types" "^17.1.0"
"@types/node@18.x":
version "18.15.13"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469"
integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==
"@vscode/extension-telemetry@^0.9.0":
version "0.9.0"
resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz#8c6c61e253ff304f46045f04edd60059b144417a"
integrity sha512-37RxGHXrs3GoXPgCUKQhghEu0gxs8j27RLjQwwtSf4WhPdJKz8UrqMYzpsXlliQ05zURYmtdGZst9C6+hfWXaQ==
dependencies:
"@microsoft/1ds-core-js" "^4.0.3"
"@microsoft/1ds-post-js" "^4.0.3"
"@microsoft/applicationinsights-web-basic" "^3.0.4"
before-after-hook@^2.2.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e"
integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==
deprecation@^2.0.0, deprecation@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
graphql-tag@^2.10.3:
version "2.12.6"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1"
integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==
dependencies:
tslib "^2.1.0"
graphql@^16.0.0:
version "16.8.1"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
is-plain-object@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
tslib@^2.1.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
tunnel@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
universal-user-agent@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"
integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=

View File

@ -1,14 +0,0 @@
.vscode/**
.vscode-test/**
out/test/**
out/**
extension.webpack.config.js
extension-browser.webpack.config.js
yarn.lock
src/**
.gitignore
vsc-extension-quickstart.md
**/tsconfig.json
**/tslint.json
**/*.map
**/*.ts

View File

@ -1,9 +0,0 @@
# Microsoft Authentication for Visual Studio Code
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
## Features
This extension provides support for authenticating to Microsoft. It registers the `microsoft` Authentication Provider that can be leveraged by other extensions. This also provides the Microsoft authentication used by Settings Sync.
Additionally, it provides the `microsoft-sovereign-cloud` Authentication Provider that can be used to sign in to other Azure clouds like Azure for US Government or Azure China. Use the setting `microsoft-sovereign-cloud.endpoint` to select the authentication endpoint the provider should use. Please note that different scopes may also be required in different environments.

View File

@ -1,31 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const path = require('path');
const withBrowserDefaults = require('../shared.webpack.config').browser;
module.exports = withBrowserDefaults({
context: __dirname,
node: {
global: true,
__filename: false,
__dirname: false,
},
entry: {
extension: './src/extension.ts',
},
resolve: {
alias: {
'./node/crypto': path.resolve(__dirname, 'src/browser/crypto'),
'./node/authServer': path.resolve(__dirname, 'src/browser/authServer'),
'./node/buffer': path.resolve(__dirname, 'src/browser/buffer'),
'./node/fetch': path.resolve(__dirname, 'src/browser/fetch'),
}
}
});

View File

@ -1,17 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withDefaults = require('../shared.webpack.config');
module.exports = withDefaults({
context: __dirname,
entry: {
extension: './src/extension.ts'
}
});

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1,37 +0,0 @@
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Microsoft Account - Sign In</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="auth.css" />
</head>
<body>
<a class="branding" href="https://code.visualstudio.com/">
Visual Studio Code
</a>
<div class="message-container">
<div class="message">
You are signed in now and can close this page.
</div>
<div class="error-message">
An error occurred while signing in:
<div class="error-text"></div>
</div>
</div>
<script>
var search = window.location.search;
var error = (/[?&^]error=([^&]+)/.exec(search) || [])[1];
if (error) {
document.querySelector('.error-text')
.textContent = decodeURIComponent(error);
document.querySelector('body')
.classList.add('error');
}
</script>
</body>
</html>

View File

@ -1,127 +0,0 @@
{
"name": "microsoft-authentication",
"publisher": "vscode",
"license": "MIT",
"displayName": "%displayName%",
"description": "%description%",
"version": "0.0.1",
"engines": {
"vscode": "^1.42.0"
},
"icon": "media/icon.png",
"categories": [
"Other"
],
"activationEvents": [],
"enabledApiProposals": [
"idToken"
],
"capabilities": {
"virtualWorkspaces": true,
"untrustedWorkspaces": {
"supported": true
}
},
"extensionKind": [
"ui",
"workspace"
],
"contributes": {
"authentication": [
{
"label": "Microsoft",
"id": "microsoft"
},
{
"label": "Microsoft Sovereign Cloud",
"id": "microsoft-sovereign-cloud"
}
],
"configuration": [
{
"title": "Microsoft Sovereign Cloud",
"properties": {
"microsoft-sovereign-cloud.environment": {
"type": "string",
"markdownDescription": "%microsoft-sovereign-cloud.environment.description%",
"enum": [
"ChinaCloud",
"USGovernment",
"custom"
],
"enumDescriptions": [
"%microsoft-sovereign-cloud.environment.enumDescriptions.AzureChinaCloud%",
"%microsoft-sovereign-cloud.environment.enumDescriptions.AzureUSGovernment%",
"%microsoft-sovereign-cloud.environment.enumDescriptions.custom%"
]
},
"microsoft-sovereign-cloud.customEnvironment": {
"type": "object",
"additionalProperties": true,
"markdownDescription": "%microsoft-sovereign-cloud.customEnvironment.description%",
"properties": {
"name": {
"type": "string",
"description": "%microsoft-sovereign-cloud.customEnvironment.name.description%"
},
"portalUrl": {
"type": "string",
"description": "%microsoft-sovereign-cloud.customEnvironment.portalUrl.description%"
},
"managementEndpointUrl": {
"type": "string",
"description": "%microsoft-sovereign-cloud.customEnvironment.managementEndpointUrl.description%"
},
"resourceManagerEndpointUrl": {
"type": "string",
"description": "%microsoft-sovereign-cloud.customEnvironment.resourceManagerEndpointUrl.description%"
},
"activeDirectoryEndpointUrl": {
"type": "string",
"description": "%microsoft-sovereign-cloud.customEnvironment.activeDirectoryEndpointUrl.description%"
},
"activeDirectoryResourceId": {
"type": "string",
"description": "%microsoft-sovereign-cloud.customEnvironment.activeDirectoryResourceId.description%"
}
},
"required": [
"name",
"portalUrl",
"managementEndpointUrl",
"resourceManagerEndpointUrl",
"activeDirectoryEndpointUrl",
"activeDirectoryResourceId"
]
}
}
}
]
},
"aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
"main": "./out/extension.js",
"browser": "./dist/browser/extension.js",
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "gulp compile-extension:microsoft-authentication",
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
"watch": "gulp watch-extension:microsoft-authentication",
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose"
},
"devDependencies": {
"@types/node": "18.x",
"@types/node-fetch": "^2.5.7",
"@types/randombytes": "^2.0.0",
"@types/sha.js": "^2.4.0",
"@types/uuid": "8.0.0"
},
"dependencies": {
"node-fetch": "2.6.7",
"@azure/ms-rest-azure-env": "^2.0.0",
"@vscode/extension-telemetry": "^0.9.0"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/vscode.git"
}
}

View File

@ -1,29 +0,0 @@
{
"displayName": "Microsoft Account",
"description": "Microsoft authentication provider",
"signIn": "Sign In",
"signOut": "Sign Out",
"microsoft-sovereign-cloud.environment.description": {
"message": "The Sovereign Cloud to use for authentication. If you select `custom`, you must also set the `#microsoft-sovereign-cloud.customEnvironment#` setting.",
"comment": [
"{Locked='`#microsoft-sovereign-cloud.customEnvironment#`'}",
"The `#microsoft-sovereign-cloud.customEnvironment#` syntax will turn into a link. Do not translate it."
]
},
"microsoft-sovereign-cloud.environment.enumDescriptions.AzureChinaCloud": "Azure China",
"microsoft-sovereign-cloud.environment.enumDescriptions.AzureUSGovernment": "Azure US Government",
"microsoft-sovereign-cloud.environment.enumDescriptions.custom": "A custom Microsoft Sovereign Cloud",
"microsoft-sovereign-cloud.customEnvironment.description": {
"message": "The custom configuration for the Sovereign Cloud to use with the Microsoft Sovereign Cloud authentication provider. This along with setting `#microsoft-sovereign-cloud.environment#` to `custom` is required to use this feature.",
"comment": [
"{Locked='`#microsoft-sovereign-cloud.environment#`'}",
"The `#microsoft-sovereign-cloud.environment#` syntax will turn into a link. Do not translate it."
]
},
"microsoft-sovereign-cloud.customEnvironment.name.description": "The name of the custom Sovereign Cloud.",
"microsoft-sovereign-cloud.customEnvironment.portalUrl.description": "The portal URL for the custom Sovereign Cloud.",
"microsoft-sovereign-cloud.customEnvironment.managementEndpointUrl.description": "The management endpoint for the custom Sovereign Cloud.",
"microsoft-sovereign-cloud.customEnvironment.resourceManagerEndpointUrl.description": "The resource manager endpoint for the custom Sovereign Cloud.",
"microsoft-sovereign-cloud.customEnvironment.activeDirectoryEndpointUrl.description": "The Active Directory endpoint for the custom Sovereign Cloud.",
"microsoft-sovereign-cloud.customEnvironment.activeDirectoryResourceId.description": "The Active Directory resource ID for the custom Sovereign Cloud."
}

View File

@ -1,953 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as path from 'path';
import { isSupportedEnvironment } from './common/uri';
import { IntervalTimer, SequencerByKey } from './common/async';
import { generateCodeChallenge, generateCodeVerifier, randomUUID } from './cryptoUtils';
import { BetterTokenStorage, IDidChangeInOtherWindowEvent } from './betterSecretStorage';
import { LoopbackAuthServer } from './node/authServer';
import { base64Decode } from './node/buffer';
import { fetching } from './node/fetch';
import { UriEventHandler } from './UriEventHandler';
import TelemetryReporter from '@vscode/extension-telemetry';
import { Environment } from '@azure/ms-rest-azure-env';
const redirectUrl = 'https://vscode.dev/redirect';
const defaultActiveDirectoryEndpointUrl = Environment.AzureCloud.activeDirectoryEndpointUrl;
const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56';
const DEFAULT_TENANT = 'organizations';
const MSA_TID = '9188040d-6c67-4c5b-b112-36a304b66dad';
const MSA_PASSTHRU_TID = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a';
const enum MicrosoftAccountType {
AAD = 'aad',
MSA = 'msa',
Unknown = 'unknown'
}
interface IToken {
accessToken?: string; // When unable to refresh due to network problems, the access token becomes undefined
idToken?: string; // depending on the scopes can be either supplied or empty
expiresIn?: number; // How long access token is valid, in seconds
expiresAt?: number; // UNIX epoch time at which token will expire
refreshToken: string;
account: {
label: string;
id: string;
type: MicrosoftAccountType;
};
scope: string;
sessionId: string; // The account id + the scope
}
export interface IStoredSession {
id: string;
refreshToken: string;
scope: string; // Scopes are alphabetized and joined with a space
account: {
label: string;
id: string;
};
endpoint: string | undefined;
}
export interface ITokenResponse {
access_token: string;
expires_in: number;
ext_expires_in: number;
refresh_token: string;
scope: string;
token_type: string;
id_token?: string;
}
export interface IMicrosoftTokens {
accessToken: string;
idToken?: string;
}
interface IScopeData {
originalScopes?: string[];
scopes: string[];
scopeStr: string;
scopesToSend: string;
clientId: string;
tenant: string;
}
export const REFRESH_NETWORK_FAILURE = 'Network failure';
export class AzureActiveDirectoryService {
// For details on why this is set to 2/3... see https://github.com/microsoft/vscode/issues/133201#issuecomment-966668197
private static REFRESH_TIMEOUT_MODIFIER = 1000 * 2 / 3;
private static POLLING_CONSTANT = 1000 * 60 * 30;
private _tokens: IToken[] = [];
private _refreshTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>();
private _sessionChangeEmitter: vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent> = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
// Used to keep track of current requests when not using the local server approach.
private _pendingNonces = new Map<string, string[]>();
private _codeExchangePromises = new Map<string, Promise<vscode.AuthenticationSession>>();
private _codeVerfifiers = new Map<string, string>();
// Used to keep track of tokens that we need to store but can't because we aren't the focused window.
private _pendingTokensToStore: Map<string, IToken> = new Map<string, IToken>();
// Used to sequence requests to the same scope.
private _sequencer = new SequencerByKey<string>();
constructor(
private readonly _logger: vscode.LogOutputChannel,
_context: vscode.ExtensionContext,
private readonly _uriHandler: UriEventHandler,
private readonly _tokenStorage: BetterTokenStorage<IStoredSession>,
private readonly _telemetryReporter: TelemetryReporter,
private readonly _env: Environment
) {
_context.subscriptions.push(this._tokenStorage.onDidChangeInOtherWindow((e) => this.checkForUpdates(e)));
_context.subscriptions.push(vscode.window.onDidChangeWindowState(async (e) => e.focused && await this.storePendingTokens()));
// In the event that a window isn't focused for a long time, we should still try to store the tokens at some point.
const timer = new IntervalTimer();
timer.cancelAndSet(
() => !vscode.window.state.focused && this.storePendingTokens(),
// 5 hours + random extra 0-30 seconds so that each window doesn't try to store at the same time
(18000000) + Math.floor(Math.random() * 30000));
_context.subscriptions.push(timer);
}
public async initialize(): Promise<void> {
this._logger.trace('Reading sessions from secret storage...');
const sessions = await this._tokenStorage.getAll(item => this.sessionMatchesEndpoint(item));
this._logger.trace(`Got ${sessions.length} stored sessions`);
const refreshes = sessions.map(async session => {
this._logger.trace(`[${session.scope}] '${session.id}' Read stored session`);
const scopes = session.scope.split(' ');
const scopeData: IScopeData = {
scopes,
scopeStr: session.scope,
// filter our special scopes
scopesToSend: scopes.filter(s => !s.startsWith('VSCODE_')).join(' '),
clientId: this.getClientId(scopes),
tenant: this.getTenantId(scopes),
};
try {
await this.refreshToken(session.refreshToken, scopeData, session.id);
} catch (e) {
// If we aren't connected to the internet, then wait and try to refresh again later.
if (e.message === REFRESH_NETWORK_FAILURE) {
this._tokens.push({
accessToken: undefined,
refreshToken: session.refreshToken,
account: {
...session.account,
type: MicrosoftAccountType.Unknown
},
scope: session.scope,
sessionId: session.id
});
} else {
vscode.window.showErrorMessage(vscode.l10n.t('You have been signed out because reading stored authentication information failed.'));
this._logger.error(e);
await this.removeSessionByIToken({
accessToken: undefined,
refreshToken: session.refreshToken,
account: {
...session.account,
type: MicrosoftAccountType.Unknown
},
scope: session.scope,
sessionId: session.id
});
}
}
});
const result = await Promise.allSettled(refreshes);
for (const res of result) {
if (res.status === 'rejected') {
this._logger.error(`Failed to initialize stored data: ${res.reason}`);
this.clearSessions();
break;
}
}
for (const token of this._tokens) {
/* __GDPR__
"login" : {
"owner": "TylerLeonhardt",
"comment": "Used to determine the usage of the Microsoft Auth Provider.",
"scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." },
"accountType": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what account types are being used." }
}
*/
this._telemetryReporter.sendTelemetryEvent('account', {
// Get rid of guids from telemetry.
scopes: JSON.stringify(token.scope.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}').split(' ')),
accountType: token.account.type
});
}
}
//#region session operations
public get onDidChangeSessions(): vscode.Event<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent> {
return this._sessionChangeEmitter.event;
}
public getSessions(scopes?: string[]): Promise<vscode.AuthenticationSession[]> {
if (!scopes) {
this._logger.info('Getting sessions for all scopes...');
const sessions = this._tokens.map(token => this.convertToSessionSync(token));
this._logger.info(`Got ${sessions.length} sessions for all scopes...`);
return Promise.resolve(sessions);
}
let modifiedScopes = [...scopes];
if (!modifiedScopes.includes('openid')) {
modifiedScopes.push('openid');
}
if (!modifiedScopes.includes('email')) {
modifiedScopes.push('email');
}
if (!modifiedScopes.includes('profile')) {
modifiedScopes.push('profile');
}
if (!modifiedScopes.includes('offline_access')) {
modifiedScopes.push('offline_access');
}
modifiedScopes = modifiedScopes.sort();
const modifiedScopesStr = modifiedScopes.join(' ');
const clientId = this.getClientId(scopes);
const scopeData: IScopeData = {
clientId,
originalScopes: scopes,
scopes: modifiedScopes,
scopeStr: modifiedScopesStr,
// filter our special scopes
scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '),
tenant: this.getTenantId(scopes),
};
this._logger.trace(`[${scopeData.scopeStr}] Queued getting sessions`);
return this._sequencer.queue(modifiedScopesStr, () => this.doGetSessions(scopeData));
}
private async doGetSessions(scopeData: IScopeData): Promise<vscode.AuthenticationSession[]> {
this._logger.info(`[${scopeData.scopeStr}] Getting sessions`);
const matchingTokens = this._tokens.filter(token => token.scope === scopeData.scopeStr);
// If we still don't have a matching token try to get a new token from an existing token by using
// the refreshToken. This is documented here:
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#refresh-the-access-token
// "Refresh tokens are valid for all permissions that your client has already received consent for."
if (!matchingTokens.length) {
// Get a token with the correct client id.
const token = scopeData.clientId === DEFAULT_CLIENT_ID
? this._tokens.find(t => t.refreshToken && !t.scope.includes('VSCODE_CLIENT_ID'))
: this._tokens.find(t => t.refreshToken && t.scope.includes(`VSCODE_CLIENT_ID:${scopeData.clientId}`));
if (token) {
this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Found a matching token with a different scopes '${token.scope}'. Attempting to get a new session using the existing session.`);
try {
const itoken = await this.doRefreshToken(token.refreshToken, scopeData);
this._sessionChangeEmitter.fire({ added: [this.convertToSessionSync(itoken)], removed: [], changed: [] });
matchingTokens.push(itoken);
} catch (err) {
this._logger.error(`[${scopeData.scopeStr}] Attempted to get a new session using the existing session with scopes '${token.scope}' but it failed due to: ${err.message ?? err}`);
}
}
}
this._logger.info(`[${scopeData.scopeStr}] Got ${matchingTokens.length} sessions`);
const results = await Promise.allSettled(matchingTokens.map(token => this.convertToSession(token, scopeData)));
return results
.filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<vscode.AuthenticationSession>).value);
}
public createSession(scopes: string[]): Promise<vscode.AuthenticationSession> {
let modifiedScopes = [...scopes];
if (!modifiedScopes.includes('openid')) {
modifiedScopes.push('openid');
}
if (!modifiedScopes.includes('email')) {
modifiedScopes.push('email');
}
if (!modifiedScopes.includes('profile')) {
modifiedScopes.push('profile');
}
if (!modifiedScopes.includes('offline_access')) {
modifiedScopes.push('offline_access');
}
modifiedScopes = modifiedScopes.sort();
const scopeData: IScopeData = {
originalScopes: scopes,
scopes: modifiedScopes,
scopeStr: modifiedScopes.join(' '),
// filter our special scopes
scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '),
clientId: this.getClientId(scopes),
tenant: this.getTenantId(scopes),
};
this._logger.trace(`[${scopeData.scopeStr}] Queued creating session`);
return this._sequencer.queue(scopeData.scopeStr, () => this.doCreateSession(scopeData));
}
private async doCreateSession(scopeData: IScopeData): Promise<vscode.AuthenticationSession> {
this._logger.info(`[${scopeData.scopeStr}] Creating session`);
const runsRemote = vscode.env.remoteName !== undefined;
const runsServerless = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Web;
if (runsServerless && this._env.activeDirectoryEndpointUrl !== defaultActiveDirectoryEndpointUrl) {
throw new Error('Sign in to non-public clouds is not supported on the web.');
}
if (runsRemote || runsServerless) {
return this.createSessionWithoutLocalServer(scopeData);
}
try {
return await this.createSessionWithLocalServer(scopeData);
} catch (e) {
this._logger.error(`[${scopeData.scopeStr}] Error creating session: ${e}`);
// 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') {
return this.createSessionWithoutLocalServer(scopeData);
}
throw e;
}
}
private async createSessionWithLocalServer(scopeData: IScopeData) {
this._logger.trace(`[${scopeData.scopeStr}] Starting login flow with local server`);
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const qs = new URLSearchParams({
response_type: 'code',
response_mode: 'query',
client_id: scopeData.clientId,
redirect_uri: redirectUrl,
scope: scopeData.scopesToSend,
prompt: 'select_account',
code_challenge_method: 'S256',
code_challenge: codeChallenge,
}).toString();
const loginUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize?${qs}`, this._env.activeDirectoryEndpointUrl).toString();
const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl);
await server.start();
let codeToExchange;
try {
vscode.env.openExternal(vscode.Uri.parse(`http://127.0.0.1:${server.port}/signin?nonce=${encodeURIComponent(server.nonce)}`));
const { code } = await server.waitForOAuthResponse();
codeToExchange = code;
} finally {
setTimeout(() => {
void server.stop();
}, 5000);
}
const session = await this.exchangeCodeForSession(codeToExchange, codeVerifier, scopeData);
this._logger.trace(`[${scopeData.scopeStr}] '${session.id}' Sending change event for added session`);
this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] });
this._logger.info(`[${scopeData.scopeStr}] '${session.id}' session successfully created!`);
return session;
}
private async createSessionWithoutLocalServer(scopeData: IScopeData): Promise<vscode.AuthenticationSession> {
this._logger.trace(`[${scopeData.scopeStr}] Starting login flow without local server`);
let callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`));
const nonce = generateCodeVerifier();
const callbackQuery = new URLSearchParams(callbackUri.query);
callbackQuery.set('nonce', encodeURIComponent(nonce));
callbackUri = callbackUri.with({
query: callbackQuery.toString()
});
const state = encodeURIComponent(callbackUri.toString(true));
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const signInUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize`, this._env.activeDirectoryEndpointUrl);
signInUrl.search = new URLSearchParams({
response_type: 'code',
client_id: encodeURIComponent(scopeData.clientId),
response_mode: 'query',
redirect_uri: redirectUrl,
state,
scope: scopeData.scopesToSend,
prompt: 'select_account',
code_challenge_method: 'S256',
code_challenge: codeChallenge,
}).toString();
const uri = vscode.Uri.parse(signInUrl.toString());
vscode.env.openExternal(uri);
let inputBox: vscode.InputBox | undefined;
const timeoutPromise = new Promise((_: (value: vscode.AuthenticationSession) => void, reject) => {
const wait = setTimeout(() => {
clearTimeout(wait);
inputBox?.dispose();
reject('Login timed out.');
}, 1000 * 60 * 5);
});
const existingNonces = this._pendingNonces.get(scopeData.scopeStr) || [];
this._pendingNonces.set(scopeData.scopeStr, [...existingNonces, nonce]);
// Register a single listener for the URI callback, in case the user starts the login process multiple times
// before completing it.
let existingPromise = this._codeExchangePromises.get(scopeData.scopeStr);
if (!existingPromise) {
if (isSupportedEnvironment(callbackUri)) {
existingPromise = this.handleCodeResponse(scopeData);
} else {
inputBox = vscode.window.createInputBox();
existingPromise = this.handleCodeInputBox(inputBox, codeVerifier, scopeData);
}
this._codeExchangePromises.set(scopeData.scopeStr, existingPromise);
}
this._codeVerfifiers.set(nonce, codeVerifier);
return Promise.race([existingPromise, timeoutPromise])
.finally(() => {
this._pendingNonces.delete(scopeData.scopeStr);
this._codeExchangePromises.delete(scopeData.scopeStr);
this._codeVerfifiers.delete(nonce);
});
}
public async removeSessionById(sessionId: string, writeToDisk: boolean = true): Promise<vscode.AuthenticationSession | undefined> {
const tokenIndex = this._tokens.findIndex(token => token.sessionId === sessionId);
if (tokenIndex === -1) {
this._logger.warn(`'${sessionId}' Session not found to remove`);
return Promise.resolve(undefined);
}
const token = this._tokens.splice(tokenIndex, 1)[0];
this._logger.trace(`[${token.scope}] '${sessionId}' Queued removing session`);
return this._sequencer.queue(token.scope, () => this.removeSessionByIToken(token, writeToDisk));
}
public async clearSessions() {
this._logger.trace('Logging out of all sessions');
this._tokens = [];
await this._tokenStorage.deleteAll(item => this.sessionMatchesEndpoint(item));
this._refreshTimeouts.forEach(timeout => {
clearTimeout(timeout);
});
this._refreshTimeouts.clear();
this._logger.trace('All sessions logged out');
}
private async removeSessionByIToken(token: IToken, writeToDisk: boolean = true): Promise<vscode.AuthenticationSession | undefined> {
this._logger.info(`[${token.scope}] '${token.sessionId}' Logging out of session`);
this.removeSessionTimeout(token.sessionId);
if (writeToDisk) {
await this._tokenStorage.delete(token.sessionId);
}
const tokenIndex = this._tokens.findIndex(t => t.sessionId === token.sessionId);
if (tokenIndex !== -1) {
this._tokens.splice(tokenIndex, 1);
}
const session = this.convertToSessionSync(token);
this._logger.trace(`[${token.scope}] '${token.sessionId}' Sending change event for session that was removed`);
this._sessionChangeEmitter.fire({ added: [], removed: [session], changed: [] });
this._logger.info(`[${token.scope}] '${token.sessionId}' Logged out of session successfully!`);
return session;
}
//#endregion
//#region timeout
private setSessionTimeout(sessionId: string, refreshToken: string, scopeData: IScopeData, timeout: number) {
this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' Setting refresh timeout for ${timeout} milliseconds`);
this.removeSessionTimeout(sessionId);
this._refreshTimeouts.set(sessionId, setTimeout(async () => {
try {
const refreshedToken = await this.refreshToken(refreshToken, scopeData, sessionId);
this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' Sending change event for session that was refreshed`);
this._sessionChangeEmitter.fire({ added: [], removed: [], changed: [this.convertToSessionSync(refreshedToken)] });
this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' refresh timeout complete`);
} catch (e) {
if (e.message !== REFRESH_NETWORK_FAILURE) {
vscode.window.showErrorMessage(vscode.l10n.t('You have been signed out because reading stored authentication information failed.'));
await this.removeSessionById(sessionId);
}
}
}, timeout));
}
private removeSessionTimeout(sessionId: string): void {
const timeout = this._refreshTimeouts.get(sessionId);
if (timeout) {
clearTimeout(timeout);
this._refreshTimeouts.delete(sessionId);
}
}
//#endregion
//#region convert operations
private convertToTokenSync(json: ITokenResponse, scopeData: IScopeData, existingId?: string): IToken {
let claims = undefined;
this._logger.trace(`[${scopeData.scopeStr}] '${existingId ?? 'new'}' Attempting to parse token response.`);
try {
if (json.id_token) {
claims = JSON.parse(base64Decode(json.id_token.split('.')[1]));
} else {
this._logger.warn(`[${scopeData.scopeStr}] '${existingId ?? 'new'}' Attempting to parse access_token instead since no id_token was included in the response.`);
claims = JSON.parse(base64Decode(json.access_token.split('.')[1]));
}
} catch (e) {
throw e;
}
const id = `${claims.tid}/${(claims.oid ?? (claims.altsecid ?? '' + claims.ipd ?? ''))}`;
const sessionId = existingId || `${id}/${randomUUID()}`;
this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' Token response parsed successfully.`);
return {
expiresIn: json.expires_in,
expiresAt: json.expires_in ? Date.now() + json.expires_in * 1000 : undefined,
accessToken: json.access_token,
idToken: json.id_token,
refreshToken: json.refresh_token,
scope: scopeData.scopeStr,
sessionId,
account: {
label: claims.preferred_username ?? claims.email ?? claims.unique_name ?? 'user@example.com',
id,
type: claims.tid === MSA_TID || claims.tid === MSA_PASSTHRU_TID ? MicrosoftAccountType.MSA : MicrosoftAccountType.AAD
}
};
}
/**
* Return a session object without checking for expiry and potentially refreshing.
* @param token The token information.
*/
private convertToSessionSync(token: IToken): vscode.AuthenticationSession {
return {
id: token.sessionId,
accessToken: token.accessToken!,
idToken: token.idToken,
account: token.account,
scopes: token.scope.split(' ')
};
}
private async convertToSession(token: IToken, scopeData: IScopeData): Promise<vscode.AuthenticationSession> {
if (token.accessToken && (!token.expiresAt || token.expiresAt > Date.now())) {
this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Token available from cache${token.expiresAt ? `, expires in ${token.expiresAt - Date.now()} milliseconds` : ''}.`);
return {
id: token.sessionId,
accessToken: token.accessToken,
idToken: token.idToken,
account: token.account,
scopes: scopeData.originalScopes ?? scopeData.scopes
};
}
try {
this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Token expired or unavailable, trying refresh`);
const refreshedToken = await this.refreshToken(token.refreshToken, scopeData, token.sessionId);
if (refreshedToken.accessToken) {
return {
id: token.sessionId,
accessToken: refreshedToken.accessToken,
idToken: refreshedToken.idToken,
account: token.account,
// We always prefer the original scopes requested since that array is used as a key in the AuthService
scopes: scopeData.originalScopes ?? scopeData.scopes
};
} else {
throw new Error();
}
} catch (e) {
throw new Error('Unavailable due to network problems');
}
}
//#endregion
//#region refresh logic
private refreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise<IToken> {
this._logger.trace(`[${scopeData.scopeStr}] '${sessionId ?? 'new'}' Queued refreshing token`);
return this._sequencer.queue(scopeData.scopeStr, () => this.doRefreshToken(refreshToken, scopeData, sessionId));
}
private async doRefreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise<IToken> {
this._logger.trace(`[${scopeData.scopeStr}] '${sessionId ?? 'new'}' Refreshing token`);
const postData = new URLSearchParams({
refresh_token: refreshToken,
client_id: scopeData.clientId,
grant_type: 'refresh_token',
scope: scopeData.scopesToSend
}).toString();
try {
const json = await this.fetchTokenResponse(postData, scopeData);
const token = this.convertToTokenSync(json, scopeData, sessionId);
if (token.expiresIn) {
this.setSessionTimeout(token.sessionId, token.refreshToken, scopeData, token.expiresIn * AzureActiveDirectoryService.REFRESH_TIMEOUT_MODIFIER);
}
this.setToken(token, scopeData);
this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Token refresh success`);
return token;
} catch (e) {
if (e.message === REFRESH_NETWORK_FAILURE) {
// We were unable to refresh because of a network failure (i.e. the user lost internet access).
// so set up a timeout to try again later. We only do this if we have a session id to reference later.
if (sessionId) {
this.setSessionTimeout(sessionId, refreshToken, scopeData, AzureActiveDirectoryService.POLLING_CONSTANT);
}
throw e;
}
this._logger.error(`[${scopeData.scopeStr}] '${sessionId ?? 'new'}' Refreshing token failed: ${e.message}`);
throw e;
}
}
//#endregion
//#region scope parsers
private getClientId(scopes: string[]) {
return scopes.reduce<string | undefined>((prev, current) => {
if (current.startsWith('VSCODE_CLIENT_ID:')) {
return current.split('VSCODE_CLIENT_ID:')[1];
}
return prev;
}, undefined) ?? DEFAULT_CLIENT_ID;
}
private getTenantId(scopes: string[]) {
return scopes.reduce<string | undefined>((prev, current) => {
if (current.startsWith('VSCODE_TENANT:')) {
return current.split('VSCODE_TENANT:')[1];
}
return prev;
}, undefined) ?? DEFAULT_TENANT;
}
//#endregion
//#region oauth flow
private async handleCodeResponse(scopeData: IScopeData): Promise<vscode.AuthenticationSession> {
let uriEventListener: vscode.Disposable;
return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => {
uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => {
try {
const query = new URLSearchParams(uri.query);
let code = query.get('code');
let nonce = query.get('nonce');
if (Array.isArray(code)) {
code = code[0];
}
if (!code) {
throw new Error('No code included in query');
}
if (Array.isArray(nonce)) {
nonce = nonce[0];
}
if (!nonce) {
throw new Error('No nonce included in query');
}
const acceptedStates = this._pendingNonces.get(scopeData.scopeStr) || [];
// Workaround double encoding issues of state in web
if (!acceptedStates.includes(nonce) && !acceptedStates.includes(decodeURIComponent(nonce))) {
throw new Error('Nonce does not match.');
}
const verifier = this._codeVerfifiers.get(nonce) ?? this._codeVerfifiers.get(decodeURIComponent(nonce));
if (!verifier) {
throw new Error('No available code verifier');
}
const session = await this.exchangeCodeForSession(code, verifier, scopeData);
this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] });
this._logger.info(`[${scopeData.scopeStr}] '${session.id}' session successfully created!`);
resolve(session);
} catch (err) {
reject(err);
}
});
}).then(result => {
uriEventListener.dispose();
return result;
}).catch(err => {
uriEventListener.dispose();
throw err;
});
}
private async handleCodeInputBox(inputBox: vscode.InputBox, verifier: string, scopeData: IScopeData): Promise<vscode.AuthenticationSession> {
this._logger.trace(`[${scopeData.scopeStr}] Starting login flow with input box`);
inputBox.ignoreFocusOut = true;
inputBox.title = vscode.l10n.t('Microsoft Authentication');
inputBox.prompt = vscode.l10n.t('Provide the authorization code to complete the sign in flow.');
inputBox.placeholder = vscode.l10n.t('Paste authorization code here...');
return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => {
inputBox.show();
inputBox.onDidAccept(async () => {
const code = inputBox.value;
if (code) {
inputBox.dispose();
const session = await this.exchangeCodeForSession(code, verifier, scopeData);
this._logger.trace(`[${scopeData.scopeStr}] '${session.id}' sending session changed event because session was added.`);
this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] });
this._logger.trace(`[${scopeData.scopeStr}] '${session.id}' session successfully created!`);
resolve(session);
}
});
inputBox.onDidHide(() => {
if (!inputBox.value) {
inputBox.dispose();
reject('Cancelled');
}
});
});
}
private async exchangeCodeForSession(code: string, codeVerifier: string, scopeData: IScopeData): Promise<vscode.AuthenticationSession> {
this._logger.trace(`[${scopeData.scopeStr}] Exchanging login code for session`);
let token: IToken | undefined;
try {
const postData = new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: scopeData.clientId,
scope: scopeData.scopesToSend,
code_verifier: codeVerifier,
redirect_uri: redirectUrl
}).toString();
const json = await this.fetchTokenResponse(postData, scopeData);
this._logger.trace(`[${scopeData.scopeStr}] Exchanging code for token succeeded!`);
token = this.convertToTokenSync(json, scopeData);
} catch (e) {
this._logger.error(`[${scopeData.scopeStr}] Error exchanging code for token: ${e}`);
throw e;
}
if (token.expiresIn) {
this.setSessionTimeout(token.sessionId, token.refreshToken, scopeData, token.expiresIn * AzureActiveDirectoryService.REFRESH_TIMEOUT_MODIFIER);
}
this.setToken(token, scopeData);
this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Exchanging login code for session succeeded!`);
return await this.convertToSession(token, scopeData);
}
private async fetchTokenResponse(postData: string, scopeData: IScopeData): Promise<ITokenResponse> {
let endpointUrl: string;
if (this._env.activeDirectoryEndpointUrl !== defaultActiveDirectoryEndpointUrl) {
// If this is for sovereign clouds, don't try using the proxy endpoint, which supports only public cloud
endpointUrl = this._env.activeDirectoryEndpointUrl;
} else {
const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
endpointUrl = proxyEndpoints?.microsoft || this._env.activeDirectoryEndpointUrl;
}
const endpoint = new URL(`${scopeData.tenant}/oauth2/v2.0/token`, endpointUrl);
let attempts = 0;
while (attempts <= 3) {
attempts++;
let result;
let errorMessage: string | undefined;
try {
result = await fetching(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length.toString()
},
body: postData
});
} catch (e) {
errorMessage = e.message ?? e;
}
if (!result || result.status > 499) {
if (attempts > 3) {
this._logger.error(`[${scopeData.scopeStr}] Fetching token failed: ${result ? await result.text() : errorMessage}`);
break;
}
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, 5 * attempts * attempts * 1000));
continue;
} else if (!result.ok) {
// For 4XX errors, the user may actually have an expired token or have changed
// their password recently which is throwing a 4XX. For this, we throw an error
// so that the user can be prompted to sign in again.
throw new Error(await result.text());
}
return await result.json() as ITokenResponse;
}
throw new Error(REFRESH_NETWORK_FAILURE);
}
//#endregion
//#region storage operations
private setToken(token: IToken, scopeData: IScopeData): void {
this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Setting 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);
}
// Don't await because setting the token is only useful for any new windows that open.
void this.storeToken(token, scopeData);
}
private async storeToken(token: IToken, scopeData: IScopeData): Promise<void> {
if (!vscode.window.state.focused) {
if (this._pendingTokensToStore.has(token.sessionId)) {
this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Window is not focused, replacing token to be stored`);
} else {
this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Window is not focused, pending storage of token`);
}
this._pendingTokensToStore.set(token.sessionId, token);
return;
}
await this._tokenStorage.store(token.sessionId, {
id: token.sessionId,
refreshToken: token.refreshToken,
scope: token.scope,
account: token.account,
endpoint: this._env.activeDirectoryEndpointUrl,
});
this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Stored token`);
}
private async storePendingTokens(): Promise<void> {
if (this._pendingTokensToStore.size === 0) {
this._logger.trace('No pending tokens to store');
return;
}
const tokens = [...this._pendingTokensToStore.values()];
this._pendingTokensToStore.clear();
this._logger.trace(`Storing ${tokens.length} pending tokens...`);
await Promise.allSettled(tokens.map(async token => {
this._logger.trace(`[${token.scope}] '${token.sessionId}' Storing pending token`);
await this._tokenStorage.store(token.sessionId, {
id: token.sessionId,
refreshToken: token.refreshToken,
scope: token.scope,
account: token.account,
endpoint: this._env.activeDirectoryEndpointUrl,
});
this._logger.trace(`[${token.scope}] '${token.sessionId}' Stored pending token`);
}));
this._logger.trace('Done storing pending tokens');
}
private async checkForUpdates(e: IDidChangeInOtherWindowEvent<IStoredSession>): Promise<void> {
for (const key of e.added) {
const session = await this._tokenStorage.get(key);
if (!session) {
this._logger.error('session not found that was apparently just added');
continue;
}
if (!this.sessionMatchesEndpoint(session)) {
// If the session wasn't made for this login endpoint, ignore this update
continue;
}
const matchesExisting = this._tokens.some(token => token.scope === session.scope && token.sessionId === session.id);
if (!matchesExisting && session.refreshToken) {
try {
const scopes = session.scope.split(' ');
const scopeData: IScopeData = {
scopes,
scopeStr: session.scope,
// filter our special scopes
scopesToSend: scopes.filter(s => !s.startsWith('VSCODE_')).join(' '),
clientId: this.getClientId(scopes),
tenant: this.getTenantId(scopes),
};
this._logger.trace(`[${scopeData.scopeStr}] '${session.id}' Session added in another window`);
const token = await this.refreshToken(session.refreshToken, scopeData, session.id);
this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Sending change event for session that was added`);
this._sessionChangeEmitter.fire({ added: [this.convertToSessionSync(token)], removed: [], changed: [] });
this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Session added in another window added here`);
continue;
} catch (e) {
// Network failures will automatically retry on next poll.
if (e.message !== REFRESH_NETWORK_FAILURE) {
vscode.window.showErrorMessage(vscode.l10n.t('You have been signed out because reading stored authentication information failed.'));
await this.removeSessionById(session.id);
}
continue;
}
}
}
for (const { value } of e.removed) {
this._logger.trace(`[${value.scope}] '${value.id}' Session removed in another window`);
if (!this.sessionMatchesEndpoint(value)) {
// If the session wasn't made for this login endpoint, ignore this update
this._logger.trace(`[${value.scope}] '${value.id}' Session doesn't match endpoint. Skipping...`);
continue;
}
await this.removeSessionById(value.id, false);
this._logger.trace(`[${value.scope}] '${value.id}' Session removed in another window removed here`);
}
// NOTE: We don't need to handle changed sessions because all that really would give us is a new refresh token
// because access tokens are not stored in Secret Storage due to their short lifespan. This new refresh token
// is not useful in this window because we really only care about the lifetime of the _access_ token which we
// are already managing (see usages of `setSessionTimeout`).
// However, in order to minimize the amount of times we store tokens, if a token was stored via another window,
// we cancel any pending token storage operations.
for (const sessionId of e.updated) {
if (this._pendingTokensToStore.delete(sessionId)) {
this._logger.trace(`'${sessionId}' Cancelled pending token storage because token was updated in another window`);
}
}
}
private sessionMatchesEndpoint(session: IStoredSession): boolean {
// For older sessions with no endpoint set, it can be assumed to be the default endpoint
session.endpoint ||= defaultActiveDirectoryEndpointUrl;
return session.endpoint === this._env.activeDirectoryEndpointUrl;
}
//#endregion
}

View File

@ -1,12 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
public handleUri(uri: vscode.Uri) {
this.fire(uri);
}
}

View File

@ -1,248 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Logger from './logger';
import { Event, EventEmitter, ExtensionContext, SecretStorage, SecretStorageChangeEvent } from 'vscode';
export interface IDidChangeInOtherWindowEvent<T> {
added: string[];
updated: string[];
removed: Array<{ key: string; value: T }>;
}
export class BetterTokenStorage<T> {
// set before and after _tokensPromise is set so getTokens can handle multiple operations.
private _operationInProgress = false;
// the current state. Don't use this directly and call getTokens() so that you ensure you
// have awaited for all operations.
private _tokensPromise: Promise<Map<string, T>> = Promise.resolve(new Map());
// The vscode SecretStorage instance for this extension.
private readonly _secretStorage: SecretStorage;
private _didChangeInOtherWindow = new EventEmitter<IDidChangeInOtherWindowEvent<T>>();
public onDidChangeInOtherWindow: Event<IDidChangeInOtherWindowEvent<T>> = this._didChangeInOtherWindow.event;
/**
*
* @param keylistKey The key in the secret storage that will hold the list of keys associated with this instance of BetterTokenStorage
* @param context the vscode Context used to register disposables and retreive the vscode.SecretStorage for this instance of VS Code
*/
constructor(private keylistKey: string, context: ExtensionContext) {
this._secretStorage = context.secrets;
context.subscriptions.push(context.secrets.onDidChange((e) => this.handleSecretChange(e)));
this.initialize();
}
private initialize(): void {
this._operationInProgress = true;
this._tokensPromise = new Promise((resolve, _) => {
this._secretStorage.get(this.keylistKey).then(
keyListStr => {
if (!keyListStr) {
resolve(new Map());
return;
}
const keyList: Array<string> = JSON.parse(keyListStr);
// Gather promises that contain key value pairs our of secret storage
const promises = keyList.map(key => new Promise<{ key: string; value: string | undefined }>((res, rej) => {
this._secretStorage.get(key).then((value) => {
res({ key, value });
}, rej);
}));
Promise.allSettled(promises).then((results => {
const tokens = new Map<string, T>();
results.forEach(p => {
if (p.status === 'fulfilled' && p.value.value) {
const secret = this.parseSecret(p.value.value);
tokens.set(p.value.key, secret);
} else if (p.status === 'rejected') {
Logger.error(p.reason);
} else {
Logger.error('Key was not found in SecretStorage.');
}
});
resolve(tokens);
}));
},
err => {
Logger.error(err);
resolve(new Map());
});
});
this._operationInProgress = false;
}
async get(key: string): Promise<T | undefined> {
const tokens = await this.getTokens();
return tokens.get(key);
}
async getAll(predicate?: (item: T) => boolean): Promise<T[]> {
const tokens = await this.getTokens();
const values = new Array<T>();
for (const [_, value] of tokens) {
if (!predicate || predicate(value)) {
values.push(value);
}
}
return values;
}
async store(key: string, value: T): Promise<void> {
const tokens = await this.getTokens();
const isAddition = !tokens.has(key);
tokens.set(key, value);
const valueStr = this.serializeSecret(value);
this._operationInProgress = true;
this._tokensPromise = new Promise((resolve, _) => {
const promises = [this._secretStorage.store(key, valueStr)];
// if we are adding a secret we need to update the keylist too
if (isAddition) {
promises.push(this.updateKeyList(tokens));
}
Promise.allSettled(promises).then(results => {
results.forEach(r => {
if (r.status === 'rejected') {
Logger.error(r.reason);
}
});
resolve(tokens);
});
});
this._operationInProgress = false;
}
async delete(key: string): Promise<void> {
const tokens = await this.getTokens();
if (!tokens.has(key)) {
return;
}
tokens.delete(key);
this._operationInProgress = true;
this._tokensPromise = new Promise((resolve, _) => {
Promise.allSettled([
this._secretStorage.delete(key),
this.updateKeyList(tokens)
]).then(results => {
results.forEach(r => {
if (r.status === 'rejected') {
Logger.error(r.reason);
}
});
resolve(tokens);
});
});
this._operationInProgress = false;
}
async deleteAll(predicate?: (item: T) => boolean): Promise<void> {
const tokens = await this.getTokens();
const promises = [];
for (const [key, value] of tokens) {
if (!predicate || predicate(value)) {
promises.push(this.delete(key));
}
}
await Promise.all(promises);
}
private async updateKeyList(tokens: Map<string, T>) {
const keyList = [];
for (const [key] of tokens) {
keyList.push(key);
}
const keyListStr = JSON.stringify(keyList);
await this._secretStorage.store(this.keylistKey, keyListStr);
}
protected parseSecret(secret: string): T {
return JSON.parse(secret);
}
protected serializeSecret(secret: T): string {
return JSON.stringify(secret);
}
// This is the one way to get tokens to ensure all other operations that
// came before you have been processed.
private async getTokens(): Promise<Map<string, T>> {
let tokens;
do {
tokens = await this._tokensPromise;
} while (this._operationInProgress);
return tokens;
}
// This is a crucial function that handles whether or not the token has changed in
// a different window of VS Code and sends the necessary event if it has.
// Scenarios this should cover:
// * Added in another window
// * Updated in another window
// * Deleted in another window
// * Added in this window
// * Updated in this window
// * Deleted in this window
private async handleSecretChange(e: SecretStorageChangeEvent) {
const key = e.key;
// The KeyList is only a list of keys to aid initial start up of VS Code to know which
// Keys are associated with this handler.
if (key === this.keylistKey) {
return;
}
const tokens = await this.getTokens();
this._operationInProgress = true;
this._tokensPromise = new Promise((resolve, _) => {
this._secretStorage.get(key).then(
storageSecretStr => {
if (!storageSecretStr) {
// true -> secret was deleted in another window
// false -> secret was deleted in this window
if (tokens.has(key)) {
const value = tokens.get(key)!;
tokens.delete(key);
this._didChangeInOtherWindow.fire({ added: [], updated: [], removed: [{ key, value }] });
}
return tokens;
}
const storageSecret = this.parseSecret(storageSecretStr);
const cachedSecret = tokens.get(key);
if (!cachedSecret) {
// token was added in another window
tokens.set(key, storageSecret);
this._didChangeInOtherWindow.fire({ added: [key], updated: [], removed: [] });
return tokens;
}
const cachedSecretStr = this.serializeSecret(cachedSecret);
if (storageSecretStr !== cachedSecretStr) {
// token was updated in another window
tokens.set(key, storageSecret);
this._didChangeInOtherWindow.fire({ added: [], updated: [key], removed: [] });
}
// what's in our token cache and what's in storage must be the same
// which means this should cover the last two scenarios of
// Added in this window & Updated in this window.
return tokens;
},
err => {
Logger.error(err);
return tokens;
}).then(resolve);
});
this._operationInProgress = false;
}
}

View File

@ -1,12 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function startServer(_: any): any {
throw new Error('Not implemented');
}
export function createServer(_: any): any {
throw new Error('Not implemented');
}

View File

@ -1,17 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function base64Encode(text: string): string {
return btoa(text);
}
export function base64Decode(text: string): string {
// modification of https://stackoverflow.com/a/38552302
const replacedCharacters = text.replace(/-/g, '+').replace(/_/g, '/');
const decodedText = decodeURIComponent(atob(replacedCharacters).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return decodedText;
}

View File

@ -1,6 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const crypto = globalThis.crypto;

View File

@ -1,6 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const fetching = fetch;

View File

@ -1,49 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vscode';
export class SequencerByKey<TKey> {
private promiseMap = new Map<TKey, Promise<unknown>>();
queue<T>(key: TKey, promiseTask: () => Promise<T>): Promise<T> {
const runningPromise = this.promiseMap.get(key) ?? Promise.resolve();
const newPromise = runningPromise
.catch(() => { })
.then(promiseTask)
.finally(() => {
if (this.promiseMap.get(key) === newPromise) {
this.promiseMap.delete(key);
}
});
this.promiseMap.set(key, newPromise);
return newPromise;
}
}
export class IntervalTimer extends Disposable {
private _token: any;
constructor() {
super(() => this.cancel());
this._token = -1;
}
cancel(): void {
if (this._token !== -1) {
clearInterval(this._token);
this._token = -1;
}
}
cancelAndSet(runner: () => void, interval: number): void {
this.cancel();
this._token = setInterval(() => {
runner();
}, interval);
}
}

Some files were not shown because too many files have changed in this diff Show More