@backstage/integration
Version:
Helpers for managing integrations towards external systems
212 lines (206 loc) • 6.81 kB
JavaScript
;
var parseGitUrl = require('git-url-parse');
var authApp = require('@octokit/auth-app');
var rest = require('@octokit/rest');
var luxon = require('luxon');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
var parseGitUrl__default = /*#__PURE__*/_interopDefaultCompat(parseGitUrl);
class Cache {
tokenCache = /* @__PURE__ */ new Map();
async getOrCreateToken(owner, repo, supplier) {
let existingInstallationData = this.tokenCache.get(owner);
if (!existingInstallationData || this.isExpired(existingInstallationData.expiresAt)) {
existingInstallationData = await supplier();
existingInstallationData.expiresAt = existingInstallationData.expiresAt.minus({ minutes: 10 });
this.tokenCache.set(owner, existingInstallationData);
}
if (!this.appliesToRepo(existingInstallationData, repo)) {
throw new Error(
`The Backstage GitHub application used in the ${owner} organization does not have access to a repository with the name ${repo}`
);
}
return { accessToken: existingInstallationData.token };
}
isExpired = (date) => luxon.DateTime.local() > date;
appliesToRepo(tokenData, repo) {
if (repo === void 0) {
return true;
}
if (tokenData.repositories !== void 0) {
return tokenData.repositories.includes(repo);
}
return true;
}
}
const HEADERS = {
Accept: "application/vnd.github.machine-man-preview+json"
};
class GithubAppManager {
appClient;
baseUrl;
baseAuthConfig;
cache = new Cache();
allowedInstallationOwners;
// undefined allows all installations
constructor(config, baseUrl) {
this.allowedInstallationOwners = config.allowedInstallationOwners;
this.baseUrl = baseUrl;
this.baseAuthConfig = {
appId: config.appId,
privateKey: config.privateKey.replace(/\\n/gm, "\n")
};
this.appClient = new rest.Octokit({
baseUrl,
headers: HEADERS,
authStrategy: authApp.createAppAuth,
auth: this.baseAuthConfig
});
}
async getInstallationCredentials(owner, repo) {
if (this.allowedInstallationOwners) {
if (!this.allowedInstallationOwners?.includes(owner)) {
return { accessToken: void 0 };
}
}
return this.cache.getOrCreateToken(owner, repo, async () => {
const { installationId, suspended } = await this.getInstallationData(
owner
);
if (suspended) {
throw new Error(`The GitHub application for ${owner} is suspended`);
}
const result = await this.appClient.apps.createInstallationAccessToken({
installation_id: installationId,
headers: HEADERS
});
let repositoryNames;
if (result.data.repository_selection === "selected") {
const installationClient = new rest.Octokit({
baseUrl: this.baseUrl,
auth: result.data.token
});
const repos = await installationClient.paginate(
installationClient.apps.listReposAccessibleToInstallation
);
const repositories = repos.repositories ?? repos;
repositoryNames = repositories.map((repository) => repository.name);
}
return {
token: result.data.token,
expiresAt: luxon.DateTime.fromISO(result.data.expires_at),
repositories: repositoryNames
};
});
}
getInstallations() {
return this.appClient.paginate(this.appClient.apps.listInstallations);
}
async getInstallationData(owner) {
const allInstallations = await this.getInstallations();
const installation = allInstallations.find(
(inst) => inst.account && "login" in inst.account && inst.account.login?.toLocaleLowerCase("en-US") === owner.toLocaleLowerCase("en-US")
);
if (installation) {
return {
installationId: installation.id,
suspended: Boolean(installation.suspended_by)
};
}
const notFoundError = new Error(
`No app installation found for ${owner} in ${this.baseAuthConfig.appId}`
);
notFoundError.name = "NotFoundError";
throw notFoundError;
}
}
class GithubAppCredentialsMux {
apps;
constructor(config) {
this.apps = config.apps?.map((ac) => new GithubAppManager(ac, config.apiBaseUrl)) ?? [];
}
async getAllInstallations() {
if (!this.apps.length) {
return [];
}
const installs = await Promise.all(
this.apps.map((app) => app.getInstallations())
);
return installs.flat();
}
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(
(resultItem) => resultItem.credentials?.accessToken
);
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 SingleInstanceGithubCredentialsProvider {
constructor(githubAppCredentialsMux, token) {
this.githubAppCredentialsMux = githubAppCredentialsMux;
this.token = token;
}
static create = (config) => {
return new SingleInstanceGithubCredentialsProvider(
new GithubAppCredentialsMux(config),
config.token
);
};
/**
* Returns {@link GithubCredentials} for a given URL.
*
* @remarks
*
* Consecutive calls to this method with the same URL will return cached
* credentials.
*
* The shortest lifetime for a token returned is 10 minutes.
*
* @example
* ```ts
* const { token, headers } = await getCredentials({
* url: 'github.com/backstage/foobar'
* })
* ```
*
* @param opts - The organization or repository URL
* @returns A promise of {@link GithubCredentials}.
*/
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 type = "app";
let token = await this.githubAppCredentialsMux.getAppToken(owner, repo);
if (!token) {
type = "token";
token = this.token;
}
return {
headers: token ? { Authorization: `Bearer ${token}` } : void 0,
token,
type
};
}
}
exports.GithubAppCredentialsMux = GithubAppCredentialsMux;
exports.SingleInstanceGithubCredentialsProvider = SingleInstanceGithubCredentialsProvider;
//# sourceMappingURL=SingleInstanceGithubCredentialsProvider.cjs.js.map