|
9 | 9 | import * as Octokit from '@octokit/rest'; |
10 | 10 | import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process'; |
11 | 11 |
|
12 | | -import {info} from '../../utils/console'; |
| 12 | +import {info, yellow} from '../../utils/console'; |
13 | 13 |
|
14 | 14 | import {MergeConfigWithRemote} from './config'; |
15 | 15 |
|
| 16 | +/** Github response type extended to include the `x-oauth-scopes` headers presence. */ |
| 17 | +type RateLimitResponseWithOAuthScopeHeader = Octokit.Response<Octokit.RateLimitGetResponse>&{ |
| 18 | + headers: {'x-oauth-scopes': string}; |
| 19 | +}; |
| 20 | + |
16 | 21 | /** Error for failed Github API requests. */ |
17 | 22 | export class GithubApiRequestError extends Error { |
18 | 23 | constructor(public status: number, message: string) { |
@@ -43,6 +48,8 @@ export class GitClient { |
43 | 48 | /** Instance of the authenticated Github octokit API. */ |
44 | 49 | api: Octokit; |
45 | 50 |
|
| 51 | + /** The OAuth scopes available for the provided Github token. */ |
| 52 | + private _oauthScopes = Promise.resolve<string[]>([]); |
46 | 53 | /** Regular expression that matches the provided Github token. */ |
47 | 54 | private _tokenRegex = new RegExp(this._githubToken, 'g'); |
48 | 55 |
|
@@ -117,4 +124,55 @@ export class GitClient { |
117 | 124 | omitGithubTokenFromMessage(value: string): string { |
118 | 125 | return value.replace(this._tokenRegex, '<TOKEN>'); |
119 | 126 | } |
| 127 | + |
| 128 | + /** |
| 129 | + * Assert the GitClient instance is using a token with permissions for the all of the |
| 130 | + * provided OAuth scopes. |
| 131 | + */ |
| 132 | + async hasOauthScopes(...requestedScopes: string[]): Promise<true|{error: string}> { |
| 133 | + const missingScopes: string[] = []; |
| 134 | + const scopes = await this.getAuthScopes(); |
| 135 | + requestedScopes.forEach(scope => { |
| 136 | + if (!scopes.includes(scope)) { |
| 137 | + missingScopes.push(scope); |
| 138 | + } |
| 139 | + }); |
| 140 | + // If no missing scopes are found, return true to indicate all OAuth Scopes are available. |
| 141 | + if (missingScopes.length === 0) { |
| 142 | + return true; |
| 143 | + } |
| 144 | + |
| 145 | + /** |
| 146 | + * Preconstructed error message to log to the user, providing missing scopes and |
| 147 | + * remediation instructions. |
| 148 | + **/ |
| 149 | + const error = |
| 150 | + `The provided <TOKEN> does not have required permissions due to missing scope(s): ` + |
| 151 | + `${yellow(missingScopes.join(', '))}\n\n` + |
| 152 | + `Update the token in use at:\n` + |
| 153 | + ` https://github.com/settings/tokens\n\n` + |
| 154 | + `Alternatively, a new token can be created at: https://github.com/settings/tokens/new\n`; |
| 155 | + |
| 156 | + return {error}; |
| 157 | + } |
| 158 | + |
| 159 | + |
| 160 | + /** |
| 161 | + * Retrieves the OAuth scopes for the loaded Github token, returning the already retrived |
| 162 | + * list of OAuth scopes if available. |
| 163 | + **/ |
| 164 | + private getAuthScopes() { |
| 165 | + // If the OAuth scopes have already been loaded, return the Promise containing them. |
| 166 | + if (this._oauthScopes) { |
| 167 | + return this._oauthScopes; |
| 168 | + } |
| 169 | + // OAuth scopes are loaded via the /rate_limit endpoint to prevent |
| 170 | + // usage of a request against that rate_limit for this lookup. |
| 171 | + this._oauthScopes = this.api.rateLimit.get().then(_response => { |
| 172 | + const response = _response as RateLimitResponseWithOAuthScopeHeader; |
| 173 | + const scopes: string = response.headers['x-oauth-scopes'] || ''; |
| 174 | + return scopes.split(',').map(scope => scope.trim()); |
| 175 | + }); |
| 176 | + return this._oauthScopes; |
| 177 | + } |
120 | 178 | } |
0 commit comments