@backstage/integration
Advanced tools
Comparing version 0.0.0-nightly-20210143754 to 0.0.0-nightly-20210193842
# @backstage/integration | ||
## 0.0.0-nightly-20210143754 | ||
## 0.0.0-nightly-20210193842 | ||
### Patch Changes | ||
- 4211198: Add support for GitHub Apps authentication for backend plugins. | ||
`GithubCredentialsProvider` requests and caches GitHub credentials based on a repository or organization url. | ||
The `GithubCredentialsProvider` class should be considered stateful since tokens will be cached internally. | ||
Consecutive calls to get credentials will return the same token, tokens older than 50 minutes will be considered expired and reissued. | ||
`GithubCredentialsProvider` will default to the configured access token if no GitHub Apps are configured. | ||
More information on how to create and configure a GitHub App to use with backstage can be found in the documentation. | ||
Usage: | ||
```javascript | ||
const credentialsProvider = new GithubCredentialsProvider(config); | ||
const { token, headers } = await credentialsProvider.getCredentials({ | ||
url: 'https://github.com/', | ||
}); | ||
``` | ||
Updates `GithubUrlReader` to use the `GithubCredentialsProvider`. | ||
## 0.2.0 | ||
### Minor Changes | ||
- a870148: Build out the `ScmIntegrations` class, as well as the individual `*Integration` classes | ||
- 466354aaa: Build out the `ScmIntegrations` class, as well as the individual `*Integration` classes | ||
@@ -9,0 +34,0 @@ ## 0.1.5 |
@@ -38,2 +38,12 @@ /* | ||
rawBaseUrl?: string; | ||
apps?: Array<{ | ||
appId: number; | ||
/** @visiblity secret */ | ||
privateKey: string; | ||
/** @visiblity secret */ | ||
webhookSecret: string; | ||
clientId: string; | ||
/** @visiblity secret */ | ||
clientSecret: string; | ||
}>; | ||
}>; | ||
@@ -40,0 +50,0 @@ |
@@ -7,2 +7,5 @@ 'use strict'; | ||
var fetch = require('cross-fetch'); | ||
var authApp = require('@octokit/auth-app'); | ||
var rest = require('@octokit/rest'); | ||
var luxon = require('luxon'); | ||
@@ -221,3 +224,3 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } | ||
function readGitHubIntegrationConfig(config2) { | ||
var _a; | ||
var _a, _b; | ||
const host = (_a = config2.getOptionalString("host")) != null ? _a : GITHUB_HOST; | ||
@@ -227,2 +230,9 @@ let apiBaseUrl = config2.getOptionalString("apiBaseUrl"); | ||
const token = config2.getOptionalString("token"); | ||
const apps = (_b = config2.getOptionalConfigArray("apps")) == null ? void 0 : _b.map((c) => ({ | ||
appId: c.getNumber("appId"), | ||
clientId: c.getString("clientId"), | ||
clientSecret: c.getString("clientSecret"), | ||
webhookSecret: c.getString("webhookSecret"), | ||
privateKey: c.getString("privateKey") | ||
})); | ||
if (!isValidHost(host)) { | ||
@@ -241,3 +251,3 @@ throw new Error(`Invalid GitHub integration config, '${host}' is not a valid host`); | ||
} | ||
return {host, apiBaseUrl, rawBaseUrl, token}; | ||
return {host, apiBaseUrl, rawBaseUrl, token, apps}; | ||
} | ||
@@ -288,2 +298,137 @@ function readGitHubIntegrationConfigs(configs) { | ||
class Cache { | ||
constructor() { | ||
this.tokenCache = new Map(); | ||
this.isNotExpired = (date) => date.diff(luxon.DateTime.local(), "minutes").minutes > 50; | ||
} | ||
async getOrCreateToken(key, supplier) { | ||
const item = this.tokenCache.get(key); | ||
if (item && this.isNotExpired(item.expiresAt)) { | ||
return {accessToken: item.token}; | ||
} | ||
const result = await supplier(); | ||
this.tokenCache.set(key, result); | ||
return {accessToken: result.token}; | ||
} | ||
} | ||
const HEADERS = { | ||
Accept: "application/vnd.github.machine-man-preview+json" | ||
}; | ||
class GithubAppManager { | ||
constructor(config2, baseUrl) { | ||
this.cache = new Cache(); | ||
this.baseAuthConfig = { | ||
appId: config2.appId, | ||
privateKey: config2.privateKey | ||
}; | ||
this.appClient = new rest.Octokit({ | ||
baseUrl, | ||
headers: HEADERS, | ||
authStrategy: authApp.createAppAuth, | ||
auth: this.baseAuthConfig | ||
}); | ||
} | ||
async getInstallationCredentials(owner, repo) { | ||
const { | ||
installationId, | ||
suspended, | ||
repositorySelection | ||
} = await this.getInstallationData(owner); | ||
if (suspended) { | ||
throw new Error(`The GitHub application for ${[owner, repo].filter(Boolean).join("/")} is suspended`); | ||
} | ||
if (repositorySelection !== "all" && !repo) { | ||
throw new Error(`The Backstage GitHub application used in the ${owner} organization must be installed for the entire organization to be able to issue credentials without a specified repository.`); | ||
} | ||
const cacheKey = !repo ? owner : `${owner}/${repo}`; | ||
const repositories = repositorySelection !== "all" ? [repo] : void 0; | ||
return this.cache.getOrCreateToken(cacheKey, async () => { | ||
const result = await this.appClient.apps.createInstallationAccessToken({ | ||
installation_id: installationId, | ||
headers: HEADERS, | ||
repositories | ||
}); | ||
return { | ||
token: result.data.token, | ||
expiresAt: luxon.DateTime.fromISO(result.data.expires_at) | ||
}; | ||
}); | ||
} | ||
async getInstallationData(owner) { | ||
var _a, _b; | ||
try { | ||
this.installations = await this.appClient.apps.listInstallations({ | ||
headers: { | ||
"If-None-Match": (_a = this.installations) == null ? void 0 : _a.headers.etag, | ||
Accept: HEADERS.Accept | ||
} | ||
}); | ||
} catch (error) { | ||
if (error.status !== 304) { | ||
throw error; | ||
} | ||
} | ||
const installation = (_b = this.installations) == null ? void 0 : _b.data.find((inst) => { | ||
var _a2; | ||
return ((_a2 = inst.account) == null ? void 0 : _a2.login) === owner; | ||
}); | ||
if (installation) { | ||
return { | ||
installationId: installation.id, | ||
suspended: Boolean(installation.suspended_by), | ||
repositorySelection: installation.repository_selection | ||
}; | ||
} | ||
const notFoundError = new Error(`No app installation found for ${owner} in ${this.baseAuthConfig.appId}`); | ||
notFoundError.name = "NotFoundError"; | ||
throw notFoundError; | ||
} | ||
} | ||
class GithubAppCredentialsMux { | ||
constructor(config2) { | ||
var _a, _b; | ||
this.apps = (_b = (_a = config2.apps) == null ? void 0 : _a.map((ac) => new GithubAppManager(ac, config2.apiBaseUrl))) != null ? _b : []; | ||
} | ||
async getAppToken(owner, repo) { | ||
if (this.apps.length === 0) { | ||
return void 0; | ||
} | ||
const results = await Promise.all(this.apps.map((app) => app.getInstallationCredentials(owner, repo).then((credentials) => ({credentials, error: void 0}), (error) => ({credentials: void 0, error})))); | ||
const result = results.find((result2) => result2.credentials); | ||
if (result) { | ||
return result.credentials.accessToken; | ||
} | ||
const errors = results.map((r) => r.error); | ||
const notNotFoundError = errors.find((err) => err.name !== "NotFoundError"); | ||
if (notNotFoundError) { | ||
throw notNotFoundError; | ||
} | ||
return void 0; | ||
} | ||
} | ||
class GithubCredentialsProvider { | ||
constructor(githubAppCredentialsMux, token) { | ||
this.githubAppCredentialsMux = githubAppCredentialsMux; | ||
this.token = token; | ||
} | ||
static create(config2) { | ||
return new GithubCredentialsProvider(new GithubAppCredentialsMux(config2), config2.token); | ||
} | ||
async getCredentials(opts) { | ||
const parsed = parseGitUrl__default['default'](opts.url); | ||
const owner = parsed.owner || parsed.name; | ||
const repo = parsed.owner ? parsed.name : void 0; | ||
let token = await this.githubAppCredentialsMux.getAppToken(owner, repo); | ||
if (!token) { | ||
token = this.token; | ||
} | ||
return { | ||
headers: token ? { | ||
Authorization: `Bearer ${token}` | ||
} : void 0, | ||
token | ||
}; | ||
} | ||
} | ||
const GITLAB_HOST = "gitlab.com"; | ||
@@ -504,2 +649,3 @@ const GITLAB_API_BASE_URL = "gitlab.com/api/v4"; | ||
exports.GithubCredentialsProvider = GithubCredentialsProvider; | ||
exports.ScmIntegrations = ScmIntegrations; | ||
@@ -506,0 +652,0 @@ exports.getAzureDownloadUrl = getAzureDownloadUrl; |
@@ -178,4 +178,37 @@ import { Config } from '@backstage/config'; | ||
token?: string; | ||
/** | ||
* The GitHub Apps configuration to use for requests to this provider. | ||
* | ||
* If no apps are specified, token or anonymous is used. | ||
*/ | ||
apps?: GithubAppConfig[]; | ||
}; | ||
/** | ||
* The configuration parameters for authenticating a GitHub Application. | ||
* A Github Apps configuration can be generated using the `backstage-cli create-github-app` command. | ||
*/ | ||
declare type GithubAppConfig = { | ||
/** | ||
* Unique app identifier, found at https://github.com/organizations/$org/settings/apps/$AppName | ||
*/ | ||
appId: number; | ||
/** | ||
* The private key is used by the GitHub App integration to authenticate the app. | ||
* A private key can be generated from the app at https://github.com/organizations/$org/settings/apps/$AppName | ||
*/ | ||
privateKey: string; | ||
/** | ||
* Webhook secret can be configured at https://github.com/organizations/$org/settings/apps/$AppName | ||
*/ | ||
webhookSecret: string; | ||
/** | ||
* Found at https://github.com/organizations/$org/settings/apps/$AppName | ||
*/ | ||
clientId: string; | ||
/** | ||
* Client secrets can be generated at https://github.com/organizations/$org/settings/apps/$AppName | ||
*/ | ||
clientSecret: string; | ||
}; | ||
/** | ||
* Reads a single GitHub integration config. | ||
@@ -214,2 +247,27 @@ * | ||
declare type GithubCredentials = { | ||
headers?: { | ||
[name: string]: string; | ||
}; | ||
token?: string; | ||
}; | ||
declare class GithubCredentialsProvider { | ||
private readonly githubAppCredentialsMux; | ||
private readonly token?; | ||
static create(config: GitHubIntegrationConfig): GithubCredentialsProvider; | ||
private constructor(); | ||
/** | ||
* Returns GithubCredentials for requested url. | ||
* Consecutive calls to this method with the same url will return cached credentials. | ||
* The shortest lifetime for a token returned is 10 minutes. | ||
* @param opts containing the organization or repository url | ||
* @returns {Promise} of @type {GithubCredentials}. | ||
* @example | ||
* const { token, headers } = await getCredentials({url: 'github.com/backstage/foobar'}) | ||
*/ | ||
getCredentials(opts: { | ||
url: string; | ||
}): Promise<GithubCredentials>; | ||
} | ||
/** | ||
@@ -379,2 +437,2 @@ * The configuration parameters for a single GitLab integration. | ||
export { AzureIntegrationConfig, BitbucketIntegrationConfig, GitHubIntegrationConfig, GitLabIntegrationConfig, ScmIntegration, ScmIntegrationRegistry, ScmIntegrations, getAzureDownloadUrl, getAzureFileFetchUrl, getAzureRequestOptions, getBitbucketDefaultBranch, getBitbucketDownloadUrl, getBitbucketFileFetchUrl, getBitbucketRequestOptions, getGitHubFileFetchUrl, getGitHubRequestOptions, getGitLabFileFetchUrl, getGitLabRequestOptions, readAzureIntegrationConfig, readAzureIntegrationConfigs, readBitbucketIntegrationConfig, readBitbucketIntegrationConfigs, readGitHubIntegrationConfig, readGitHubIntegrationConfigs, readGitLabIntegrationConfig, readGitLabIntegrationConfigs }; | ||
export { AzureIntegrationConfig, BitbucketIntegrationConfig, GitHubIntegrationConfig, GitLabIntegrationConfig, GithubCredentialsProvider, ScmIntegration, ScmIntegrationRegistry, ScmIntegrations, getAzureDownloadUrl, getAzureFileFetchUrl, getAzureRequestOptions, getBitbucketDefaultBranch, getBitbucketDownloadUrl, getBitbucketFileFetchUrl, getBitbucketRequestOptions, getGitHubFileFetchUrl, getGitHubRequestOptions, getGitLabFileFetchUrl, getGitLabRequestOptions, readAzureIntegrationConfig, readAzureIntegrationConfigs, readBitbucketIntegrationConfig, readBitbucketIntegrationConfigs, readGitHubIntegrationConfig, readGitHubIntegrationConfigs, readGitLabIntegrationConfig, readGitLabIntegrationConfigs }; |
import parseGitUrl from 'git-url-parse'; | ||
import fetch from 'cross-fetch'; | ||
import { createAppAuth } from '@octokit/auth-app'; | ||
import { Octokit } from '@octokit/rest'; | ||
import { DateTime } from 'luxon'; | ||
@@ -211,3 +214,3 @@ function isValidHost(url) { | ||
function readGitHubIntegrationConfig(config2) { | ||
var _a; | ||
var _a, _b; | ||
const host = (_a = config2.getOptionalString("host")) != null ? _a : GITHUB_HOST; | ||
@@ -217,2 +220,9 @@ let apiBaseUrl = config2.getOptionalString("apiBaseUrl"); | ||
const token = config2.getOptionalString("token"); | ||
const apps = (_b = config2.getOptionalConfigArray("apps")) == null ? void 0 : _b.map((c) => ({ | ||
appId: c.getNumber("appId"), | ||
clientId: c.getString("clientId"), | ||
clientSecret: c.getString("clientSecret"), | ||
webhookSecret: c.getString("webhookSecret"), | ||
privateKey: c.getString("privateKey") | ||
})); | ||
if (!isValidHost(host)) { | ||
@@ -231,3 +241,3 @@ throw new Error(`Invalid GitHub integration config, '${host}' is not a valid host`); | ||
} | ||
return {host, apiBaseUrl, rawBaseUrl, token}; | ||
return {host, apiBaseUrl, rawBaseUrl, token, apps}; | ||
} | ||
@@ -278,2 +288,137 @@ function readGitHubIntegrationConfigs(configs) { | ||
class Cache { | ||
constructor() { | ||
this.tokenCache = new Map(); | ||
this.isNotExpired = (date) => date.diff(DateTime.local(), "minutes").minutes > 50; | ||
} | ||
async getOrCreateToken(key, supplier) { | ||
const item = this.tokenCache.get(key); | ||
if (item && this.isNotExpired(item.expiresAt)) { | ||
return {accessToken: item.token}; | ||
} | ||
const result = await supplier(); | ||
this.tokenCache.set(key, result); | ||
return {accessToken: result.token}; | ||
} | ||
} | ||
const HEADERS = { | ||
Accept: "application/vnd.github.machine-man-preview+json" | ||
}; | ||
class GithubAppManager { | ||
constructor(config2, baseUrl) { | ||
this.cache = new Cache(); | ||
this.baseAuthConfig = { | ||
appId: config2.appId, | ||
privateKey: config2.privateKey | ||
}; | ||
this.appClient = new Octokit({ | ||
baseUrl, | ||
headers: HEADERS, | ||
authStrategy: createAppAuth, | ||
auth: this.baseAuthConfig | ||
}); | ||
} | ||
async getInstallationCredentials(owner, repo) { | ||
const { | ||
installationId, | ||
suspended, | ||
repositorySelection | ||
} = await this.getInstallationData(owner); | ||
if (suspended) { | ||
throw new Error(`The GitHub application for ${[owner, repo].filter(Boolean).join("/")} is suspended`); | ||
} | ||
if (repositorySelection !== "all" && !repo) { | ||
throw new Error(`The Backstage GitHub application used in the ${owner} organization must be installed for the entire organization to be able to issue credentials without a specified repository.`); | ||
} | ||
const cacheKey = !repo ? owner : `${owner}/${repo}`; | ||
const repositories = repositorySelection !== "all" ? [repo] : void 0; | ||
return this.cache.getOrCreateToken(cacheKey, async () => { | ||
const result = await this.appClient.apps.createInstallationAccessToken({ | ||
installation_id: installationId, | ||
headers: HEADERS, | ||
repositories | ||
}); | ||
return { | ||
token: result.data.token, | ||
expiresAt: DateTime.fromISO(result.data.expires_at) | ||
}; | ||
}); | ||
} | ||
async getInstallationData(owner) { | ||
var _a, _b; | ||
try { | ||
this.installations = await this.appClient.apps.listInstallations({ | ||
headers: { | ||
"If-None-Match": (_a = this.installations) == null ? void 0 : _a.headers.etag, | ||
Accept: HEADERS.Accept | ||
} | ||
}); | ||
} catch (error) { | ||
if (error.status !== 304) { | ||
throw error; | ||
} | ||
} | ||
const installation = (_b = this.installations) == null ? void 0 : _b.data.find((inst) => { | ||
var _a2; | ||
return ((_a2 = inst.account) == null ? void 0 : _a2.login) === owner; | ||
}); | ||
if (installation) { | ||
return { | ||
installationId: installation.id, | ||
suspended: Boolean(installation.suspended_by), | ||
repositorySelection: installation.repository_selection | ||
}; | ||
} | ||
const notFoundError = new Error(`No app installation found for ${owner} in ${this.baseAuthConfig.appId}`); | ||
notFoundError.name = "NotFoundError"; | ||
throw notFoundError; | ||
} | ||
} | ||
class GithubAppCredentialsMux { | ||
constructor(config2) { | ||
var _a, _b; | ||
this.apps = (_b = (_a = config2.apps) == null ? void 0 : _a.map((ac) => new GithubAppManager(ac, config2.apiBaseUrl))) != null ? _b : []; | ||
} | ||
async getAppToken(owner, repo) { | ||
if (this.apps.length === 0) { | ||
return void 0; | ||
} | ||
const results = await Promise.all(this.apps.map((app) => app.getInstallationCredentials(owner, repo).then((credentials) => ({credentials, error: void 0}), (error) => ({credentials: void 0, error})))); | ||
const result = results.find((result2) => result2.credentials); | ||
if (result) { | ||
return result.credentials.accessToken; | ||
} | ||
const errors = results.map((r) => r.error); | ||
const notNotFoundError = errors.find((err) => err.name !== "NotFoundError"); | ||
if (notNotFoundError) { | ||
throw notNotFoundError; | ||
} | ||
return void 0; | ||
} | ||
} | ||
class GithubCredentialsProvider { | ||
constructor(githubAppCredentialsMux, token) { | ||
this.githubAppCredentialsMux = githubAppCredentialsMux; | ||
this.token = token; | ||
} | ||
static create(config2) { | ||
return new GithubCredentialsProvider(new GithubAppCredentialsMux(config2), config2.token); | ||
} | ||
async getCredentials(opts) { | ||
const parsed = parseGitUrl(opts.url); | ||
const owner = parsed.owner || parsed.name; | ||
const repo = parsed.owner ? parsed.name : void 0; | ||
let token = await this.githubAppCredentialsMux.getAppToken(owner, repo); | ||
if (!token) { | ||
token = this.token; | ||
} | ||
return { | ||
headers: token ? { | ||
Authorization: `Bearer ${token}` | ||
} : void 0, | ||
token | ||
}; | ||
} | ||
} | ||
const GITLAB_HOST = "gitlab.com"; | ||
@@ -494,3 +639,3 @@ const GITLAB_API_BASE_URL = "gitlab.com/api/v4"; | ||
export { ScmIntegrations, getAzureDownloadUrl, getAzureFileFetchUrl, getAzureRequestOptions, getBitbucketDefaultBranch, getBitbucketDownloadUrl, getBitbucketFileFetchUrl, getBitbucketRequestOptions, getGitHubFileFetchUrl, getGitHubRequestOptions, getGitLabFileFetchUrl, getGitLabRequestOptions, readAzureIntegrationConfig, readAzureIntegrationConfigs, readBitbucketIntegrationConfig, readBitbucketIntegrationConfigs, readGitHubIntegrationConfig, readGitHubIntegrationConfigs, readGitLabIntegrationConfig, readGitLabIntegrationConfigs }; | ||
export { GithubCredentialsProvider, ScmIntegrations, getAzureDownloadUrl, getAzureFileFetchUrl, getAzureRequestOptions, getBitbucketDefaultBranch, getBitbucketDownloadUrl, getBitbucketFileFetchUrl, getBitbucketRequestOptions, getGitHubFileFetchUrl, getGitHubRequestOptions, getGitLabFileFetchUrl, getGitLabRequestOptions, readAzureIntegrationConfig, readAzureIntegrationConfigs, readBitbucketIntegrationConfig, readBitbucketIntegrationConfigs, readGitHubIntegrationConfig, readGitHubIntegrationConfigs, readGitLabIntegrationConfig, readGitLabIntegrationConfigs }; | ||
//# sourceMappingURL=index.esm.js.map |
{ | ||
"name": "@backstage/integration", | ||
"version": "0.0.0-nightly-20210143754", | ||
"version": "0.0.0-nightly-20210193842", | ||
"main": "dist/index.cjs.js", | ||
@@ -34,8 +34,12 @@ "types": "dist/index.d.ts", | ||
"cross-fetch": "^3.0.6", | ||
"git-url-parse": "^11.4.3" | ||
"git-url-parse": "^11.4.3", | ||
"@octokit/rest": "^18.0.12", | ||
"@octokit/auth-app": "^2.10.5", | ||
"luxon": "^1.25.0" | ||
}, | ||
"devDependencies": { | ||
"@backstage/cli": "^0.0.0-nightly-20210143754", | ||
"@backstage/cli": "^0.0.0-nightly-20210193842", | ||
"@backstage/test-utils": "^0.1.5", | ||
"@types/jest": "^26.0.7", | ||
"@types/luxon": "^1.25.0", | ||
"msw": "^0.21.2" | ||
@@ -42,0 +46,0 @@ }, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
181625
1739
6
5
+ Added@octokit/auth-app@^2.10.5
+ Added@octokit/rest@^18.0.12
+ Addedluxon@^1.25.0
+ Added@octokit/auth-app@2.11.0(transitive)
+ Added@octokit/auth-token@2.5.0(transitive)
+ Added@octokit/core@3.6.0(transitive)
+ Added@octokit/endpoint@6.0.12(transitive)
+ Added@octokit/graphql@4.8.0(transitive)
+ Added@octokit/openapi-types@12.11.0(transitive)
+ Added@octokit/plugin-paginate-rest@2.21.3(transitive)
+ Added@octokit/plugin-request-log@1.0.4(transitive)
+ Added@octokit/plugin-rest-endpoint-methods@5.16.2(transitive)
+ Added@octokit/request@5.6.3(transitive)
+ Added@octokit/request-error@2.1.0(transitive)
+ Added@octokit/rest@18.12.0(transitive)
+ Added@octokit/types@6.41.0(transitive)
+ Added@types/jsonwebtoken@9.0.7(transitive)
+ Added@types/lru-cache@5.1.1(transitive)
+ Added@types/node@22.10.7(transitive)
+ Addedbefore-after-hook@2.2.3(transitive)
+ Addedbuffer-equal-constant-time@1.0.1(transitive)
+ Addeddeprecation@2.3.1(transitive)
+ Addedecdsa-sig-formatter@1.0.11(transitive)
+ Addedis-plain-object@5.0.0(transitive)
+ Addedjsonwebtoken@9.0.2(transitive)
+ Addedjwa@1.4.1(transitive)
+ Addedjws@3.2.2(transitive)
+ Addedlodash.includes@4.3.0(transitive)
+ Addedlodash.isboolean@3.0.3(transitive)
+ Addedlodash.isinteger@4.0.4(transitive)
+ Addedlodash.isnumber@3.0.3(transitive)
+ Addedlodash.isplainobject@4.0.6(transitive)
+ Addedlodash.isstring@4.0.1(transitive)
+ Addedlodash.once@4.1.1(transitive)
+ Addedlru-cache@6.0.0(transitive)
+ Addedluxon@1.28.1(transitive)
+ Addedms@2.1.3(transitive)
+ Addedonce@1.4.0(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedsemver@7.6.3(transitive)
+ Addedundici-types@6.20.0(transitive)
+ Addeduniversal-github-app-jwt@1.2.0(transitive)
+ Addeduniversal-user-agent@6.0.1(transitive)
+ Addedwrappy@1.0.2(transitive)
+ Addedyallist@4.0.0(transitive)