Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@backstage/integration

Package Overview
Dependencies
Maintainers
4
Versions
984
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@backstage/integration - npm Package Compare versions

Comparing version 0.0.0-nightly-20210143754 to 0.0.0-nightly-20210193842

29

CHANGELOG.md
# @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

10

package.json
{
"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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc