@backstage/integration 2.1.0-next.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @backstage/integration
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - dc951d4: Added support for creating a GitHub credentials provider backed by the connections service.
8
+
9
+ ### Patch Changes
10
+
11
+ - 87bfe22: GitHub integrations now cache the list of app installations for a short period, avoiding a full `GET /app/installations` pagination on every token fetch. This significantly reduces API usage against the 15k/hour GitHub App rate limit for organizations with many installations or frequent credential refreshes.
12
+
13
+ The cache is refreshed on a 10-minute TTL, and is additionally invalidated when a lookup for a previously-unseen owner occurs (throttled to once per minute) or when GitHub reports that a cached installation is no longer available, so newly added or removed installations are still picked up promptly.
14
+
15
+ - Updated dependencies
16
+ - @backstage/connections@0.3.0
17
+
18
+ ## 2.1.0-next.1
19
+
20
+ ### Patch Changes
21
+
22
+ - 87bfe22: GitHub integrations now cache the list of app installations for a short period, avoiding a full `GET /app/installations` pagination on every token fetch. This significantly reduces API usage against the 15k/hour GitHub App rate limit for organizations with many installations or frequent credential refreshes.
23
+
24
+ The cache is refreshed on a 10-minute TTL, and is additionally invalidated when a lookup for a previously-unseen owner occurs (throttled to once per minute) or when GitHub reports that a cached installation is no longer available, so newly added or removed installations are still picked up promptly.
25
+
26
+ - Updated dependencies
27
+ - @backstage/connections@0.3.0-next.2
28
+
3
29
  ## 2.1.0-next.0
4
30
 
5
31
  ### Minor Changes
@@ -58,7 +58,7 @@ class DefaultGithubCredentialsProvider {
58
58
  if (this.#connections) {
59
59
  const connection = await this.#connections.find({
60
60
  type: "github",
61
- url: opts.url,
61
+ query: { url: opts.url },
62
62
  authMethods: ["app", "token", "none"]
63
63
  }).catch((error) => {
64
64
  throw new errors.ForwardedError(
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultGithubCredentialsProvider.cjs.js","sources":["../../src/github/DefaultGithubCredentialsProvider.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { GithubCredentials, GithubCredentialsProvider } from './types';\nimport { ScmIntegrationRegistry } from '../registry';\nimport { SingleInstanceGithubCredentialsProvider } from './SingleInstanceGithubCredentialsProvider';\nimport type { ConnectionsService } from '@backstage/connections';\nimport { ForwardedError, InputError } from '@backstage/errors';\nimport type { GithubIntegrationConfig } from './config';\n\n/**\n * Handles the creation and caching of credentials for GitHub integrations.\n *\n * @public\n * @remarks\n *\n * TODO: Possibly move this to a backend only package so that it's not used in the frontend by mistake\n */\nexport class DefaultGithubCredentialsProvider\n implements GithubCredentialsProvider\n{\n static fromIntegrations(integrations: ScmIntegrationRegistry) {\n const credentialsProviders: Map<string, GithubCredentialsProvider> =\n new Map<string, GithubCredentialsProvider>();\n\n integrations.github.list().forEach(integration => {\n const credentialsProvider =\n SingleInstanceGithubCredentialsProvider.create(integration.config);\n credentialsProviders.set(integration.config.host, credentialsProvider);\n });\n return new DefaultGithubCredentialsProvider(credentialsProviders);\n }\n\n /**\n * Creates a credentials provider backed by the connections service.\n *\n * @param connections - The connections service used to resolve GitHub credentials.\n * @internal\n */\n static experimentalFromConnections(connections: ConnectionsService) {\n return new DefaultGithubCredentialsProvider(\n new Map<string, GithubCredentialsProvider>(),\n connections,\n );\n }\n\n private readonly providers: Map<string, GithubCredentialsProvider>;\n readonly #connections?: ConnectionsService;\n\n private constructor(\n providers: Map<string, GithubCredentialsProvider>,\n connections?: ConnectionsService,\n ) {\n this.providers = providers;\n this.#connections = connections;\n }\n\n /**\n * Returns {@link GithubCredentials} for a given URL.\n *\n * @remarks\n *\n * Consecutive calls to this method with the same URL will return cached\n * credentials.\n *\n * The shortest lifetime for a token returned is 10 minutes.\n *\n * @example\n * ```ts\n * const { token, headers } = await getCredentials({\n * url: 'https://github.com/backstage/foobar'\n * })\n *\n * const { token, headers } = await getCredentials({\n * url: 'https://github.com/backstage'\n * })\n * ```\n *\n * @param opts - The organization or repository URL\n * @returns A promise of {@link GithubCredentials}.\n */\n async getCredentials(opts: { url: string }): Promise<GithubCredentials> {\n if (this.#connections) {\n // Ask the connections service to select auth for this URL. A host may\n // have different GitHub Apps for different organizations, so this\n // selection cannot be done once when the provider is created.\n\n const connection = await this.#connections\n .find({\n type: 'github',\n url: opts.url,\n authMethods: ['app', 'token', 'none'],\n })\n .catch(error => {\n throw new ForwardedError(\n 'Failed getting credentials from connection',\n error,\n );\n });\n\n const { auth } = connection;\n\n // Adapt the connection schema to the existing provider configuration so\n // credential creation and token caching stay in one implementation.\n const config: GithubIntegrationConfig = {\n host: connection.host,\n apiBaseUrl: connection.apiBaseUrl,\n rawBaseUrl: connection.rawBaseUrl,\n };\n\n // Reusing an App provider preserves its installation-token cache. App\n // organizations are canonicalized because GitHub owner matching is\n // case-insensitive and does not depend on ordering. Static token\n // providers have no internal token cache, so they are recreated rather\n // than placing their secret token in a cache key.\n let providerKey: string | undefined;\n if (auth.method === 'app') {\n const appId = Number(auth.appId);\n if (!Number.isSafeInteger(appId) || appId <= 0) {\n throw new InputError(\n `Invalid GitHub App ID \"${auth.appId}\", expected a positive safe integer`,\n );\n }\n const normalizedOrgs = auth.orgs?.length\n ? Array.from(\n new Set(auth.orgs.map(org => org.toLocaleLowerCase('en-US'))),\n ).sort()\n : undefined;\n config.apps = [\n {\n appId,\n privateKey: auth.privateKey,\n clientId: auth.clientId,\n clientSecret: auth.clientSecret,\n webhookSecret: auth.webhookSecret,\n publicAccess: auth.publicAccess,\n allowedInstallationOwners: normalizedOrgs,\n },\n ];\n providerKey = `${connection.host}:app:${appId}:${JSON.stringify(\n normalizedOrgs ?? [],\n )}:${String(auth.publicAccess ?? false)}`;\n } else if (auth.method === 'token') {\n config.token = auth.token;\n } else {\n providerKey = `${connection.host}:none`;\n }\n\n let provider = providerKey ? this.providers.get(providerKey) : undefined;\n if (!provider) {\n provider = SingleInstanceGithubCredentialsProvider.create(config);\n if (providerKey) {\n this.providers.set(providerKey, provider);\n }\n }\n\n return provider.getCredentials(opts);\n }\n\n const parsed = new URL(opts.url);\n const provider = this.providers.get(parsed.host);\n\n if (!provider) {\n throw new Error(\n `There is no GitHub integration that matches ${opts.url}. Please add a configuration for an integration.`,\n );\n }\n\n return provider.getCredentials(opts);\n }\n}\n"],"names":["SingleInstanceGithubCredentialsProvider","ForwardedError","InputError","provider"],"mappings":";;;;;AA+BO,MAAM,gCAAA,CAEb;AAAA,EACE,OAAO,iBAAiB,YAAA,EAAsC;AAC5D,IAAA,MAAM,oBAAA,uBACA,GAAA,EAAuC;AAE7C,IAAA,YAAA,CAAa,MAAA,CAAO,IAAA,EAAK,CAAE,OAAA,CAAQ,CAAA,WAAA,KAAe;AAChD,MAAA,MAAM,mBAAA,GACJA,+EAAA,CAAwC,MAAA,CAAO,WAAA,CAAY,MAAM,CAAA;AACnE,MAAA,oBAAA,CAAqB,GAAA,CAAI,WAAA,CAAY,MAAA,CAAO,IAAA,EAAM,mBAAmB,CAAA;AAAA,IACvE,CAAC,CAAA;AACD,IAAA,OAAO,IAAI,iCAAiC,oBAAoB,CAAA;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAO,4BAA4B,WAAA,EAAiC;AAClE,IAAA,OAAO,IAAI,gCAAA;AAAA,0BACL,GAAA,EAAuC;AAAA,MAC3C;AAAA,KACF;AAAA,EACF;AAAA,EAEiB,SAAA;AAAA,EACR,YAAA;AAAA,EAED,WAAA,CACN,WACA,WAAA,EACA;AACA,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AACjB,IAAA,IAAA,CAAK,YAAA,GAAe,WAAA;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA0BA,MAAM,eAAe,IAAA,EAAmD;AACtE,IAAA,IAAI,KAAK,YAAA,EAAc;AAKrB,MAAA,MAAM,UAAA,GAAa,MAAM,IAAA,CAAK,YAAA,CAC3B,IAAA,CAAK;AAAA,QACJ,IAAA,EAAM,QAAA;AAAA,QACN,KAAK,IAAA,CAAK,GAAA;AAAA,QACV,WAAA,EAAa,CAAC,KAAA,EAAO,OAAA,EAAS,MAAM;AAAA,OACrC,CAAA,CACA,KAAA,CAAM,CAAA,KAAA,KAAS;AACd,QAAA,MAAM,IAAIC,qBAAA;AAAA,UACR,4CAAA;AAAA,UACA;AAAA,SACF;AAAA,MACF,CAAC,CAAA;AAEH,MAAA,MAAM,EAAE,MAAK,GAAI,UAAA;AAIjB,MAAA,MAAM,MAAA,GAAkC;AAAA,QACtC,MAAM,UAAA,CAAW,IAAA;AAAA,QACjB,YAAY,UAAA,CAAW,UAAA;AAAA,QACvB,YAAY,UAAA,CAAW;AAAA,OACzB;AAOA,MAAA,IAAI,WAAA;AACJ,MAAA,IAAI,IAAA,CAAK,WAAW,KAAA,EAAO;AACzB,QAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA;AAC/B,QAAA,IAAI,CAAC,MAAA,CAAO,aAAA,CAAc,KAAK,CAAA,IAAK,SAAS,CAAA,EAAG;AAC9C,UAAA,MAAM,IAAIC,iBAAA;AAAA,YACR,CAAA,uBAAA,EAA0B,KAAK,KAAK,CAAA,mCAAA;AAAA,WACtC;AAAA,QACF;AACA,QAAA,MAAM,cAAA,GAAiB,IAAA,CAAK,IAAA,EAAM,MAAA,GAC9B,KAAA,CAAM,IAAA;AAAA,UACJ,IAAI,GAAA,CAAI,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,SAAO,GAAA,CAAI,iBAAA,CAAkB,OAAO,CAAC,CAAC;AAAA,SAC9D,CAAE,MAAK,GACP,MAAA;AACJ,QAAA,MAAA,CAAO,IAAA,GAAO;AAAA,UACZ;AAAA,YACE,KAAA;AAAA,YACA,YAAY,IAAA,CAAK,UAAA;AAAA,YACjB,UAAU,IAAA,CAAK,QAAA;AAAA,YACf,cAAc,IAAA,CAAK,YAAA;AAAA,YACnB,eAAe,IAAA,CAAK,aAAA;AAAA,YACpB,cAAc,IAAA,CAAK,YAAA;AAAA,YACnB,yBAAA,EAA2B;AAAA;AAC7B,SACF;AACA,QAAA,WAAA,GAAc,GAAG,UAAA,CAAW,IAAI,CAAA,KAAA,EAAQ,KAAK,IAAI,IAAA,CAAK,SAAA;AAAA,UACpD,kBAAkB;AAAC,SACpB,CAAA,CAAA,EAAI,MAAA,CAAO,IAAA,CAAK,YAAA,IAAgB,KAAK,CAAC,CAAA,CAAA;AAAA,MACzC,CAAA,MAAA,IAAW,IAAA,CAAK,MAAA,KAAW,OAAA,EAAS;AAClC,QAAA,MAAA,CAAO,QAAQ,IAAA,CAAK,KAAA;AAAA,MACtB,CAAA,MAAO;AACL,QAAA,WAAA,GAAc,CAAA,EAAG,WAAW,IAAI,CAAA,KAAA,CAAA;AAAA,MAClC;AAEA,MAAA,IAAIC,YAAW,WAAA,GAAc,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,WAAW,CAAA,GAAI,MAAA;AAC/D,MAAA,IAAI,CAACA,SAAAA,EAAU;AACb,QAAAA,SAAAA,GAAWH,+EAAA,CAAwC,MAAA,CAAO,MAAM,CAAA;AAChE,QAAA,IAAI,WAAA,EAAa;AACf,UAAA,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,WAAA,EAAaG,SAAQ,CAAA;AAAA,QAC1C;AAAA,MACF;AAEA,MAAA,OAAOA,SAAAA,CAAS,eAAe,IAAI,CAAA;AAAA,IACrC;AAEA,IAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,IAAA,CAAK,GAAG,CAAA;AAC/B,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,OAAO,IAAI,CAAA;AAE/C,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,4CAAA,EAA+C,KAAK,GAAG,CAAA,gDAAA;AAAA,OACzD;AAAA,IACF;AAEA,IAAA,OAAO,QAAA,CAAS,eAAe,IAAI,CAAA;AAAA,EACrC;AACF;;;;"}
1
+ {"version":3,"file":"DefaultGithubCredentialsProvider.cjs.js","sources":["../../src/github/DefaultGithubCredentialsProvider.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { GithubCredentials, GithubCredentialsProvider } from './types';\nimport { ScmIntegrationRegistry } from '../registry';\nimport { SingleInstanceGithubCredentialsProvider } from './SingleInstanceGithubCredentialsProvider';\nimport type { ConnectionsService } from '@backstage/connections';\nimport { ForwardedError, InputError } from '@backstage/errors';\nimport type { GithubIntegrationConfig } from './config';\n\n/**\n * Handles the creation and caching of credentials for GitHub integrations.\n *\n * @public\n * @remarks\n *\n * TODO: Possibly move this to a backend only package so that it's not used in the frontend by mistake\n */\nexport class DefaultGithubCredentialsProvider\n implements GithubCredentialsProvider\n{\n static fromIntegrations(integrations: ScmIntegrationRegistry) {\n const credentialsProviders: Map<string, GithubCredentialsProvider> =\n new Map<string, GithubCredentialsProvider>();\n\n integrations.github.list().forEach(integration => {\n const credentialsProvider =\n SingleInstanceGithubCredentialsProvider.create(integration.config);\n credentialsProviders.set(integration.config.host, credentialsProvider);\n });\n return new DefaultGithubCredentialsProvider(credentialsProviders);\n }\n\n /**\n * Creates a credentials provider backed by the connections service.\n *\n * @param connections - The connections service used to resolve GitHub credentials.\n * @internal\n */\n static experimentalFromConnections(connections: ConnectionsService) {\n return new DefaultGithubCredentialsProvider(\n new Map<string, GithubCredentialsProvider>(),\n connections,\n );\n }\n\n private readonly providers: Map<string, GithubCredentialsProvider>;\n readonly #connections?: ConnectionsService;\n\n private constructor(\n providers: Map<string, GithubCredentialsProvider>,\n connections?: ConnectionsService,\n ) {\n this.providers = providers;\n this.#connections = connections;\n }\n\n /**\n * Returns {@link GithubCredentials} for a given URL.\n *\n * @remarks\n *\n * Consecutive calls to this method with the same URL will return cached\n * credentials.\n *\n * The shortest lifetime for a token returned is 10 minutes.\n *\n * @example\n * ```ts\n * const { token, headers } = await getCredentials({\n * url: 'https://github.com/backstage/foobar'\n * })\n *\n * const { token, headers } = await getCredentials({\n * url: 'https://github.com/backstage'\n * })\n * ```\n *\n * @param opts - The organization or repository URL\n * @returns A promise of {@link GithubCredentials}.\n */\n async getCredentials(opts: { url: string }): Promise<GithubCredentials> {\n if (this.#connections) {\n // Ask the connections service to select auth for this URL. A host may\n // have different GitHub Apps for different organizations, so this\n // selection cannot be done once when the provider is created.\n\n const connection = await this.#connections\n .find({\n type: 'github',\n query: { url: opts.url },\n authMethods: ['app', 'token', 'none'],\n })\n .catch(error => {\n throw new ForwardedError(\n 'Failed getting credentials from connection',\n error,\n );\n });\n\n const { auth } = connection;\n\n // Adapt the connection schema to the existing provider configuration so\n // credential creation and token caching stay in one implementation.\n const config: GithubIntegrationConfig = {\n host: connection.host,\n apiBaseUrl: connection.apiBaseUrl,\n rawBaseUrl: connection.rawBaseUrl,\n };\n\n // Reusing an App provider preserves its installation-token cache. App\n // organizations are canonicalized because GitHub owner matching is\n // case-insensitive and does not depend on ordering. Static token\n // providers have no internal token cache, so they are recreated rather\n // than placing their secret token in a cache key.\n let providerKey: string | undefined;\n if (auth.method === 'app') {\n const appId = Number(auth.appId);\n if (!Number.isSafeInteger(appId) || appId <= 0) {\n throw new InputError(\n `Invalid GitHub App ID \"${auth.appId}\", expected a positive safe integer`,\n );\n }\n const normalizedOrgs = auth.orgs?.length\n ? Array.from(\n new Set(auth.orgs.map(org => org.toLocaleLowerCase('en-US'))),\n ).sort()\n : undefined;\n config.apps = [\n {\n appId,\n privateKey: auth.privateKey,\n clientId: auth.clientId,\n clientSecret: auth.clientSecret,\n webhookSecret: auth.webhookSecret,\n publicAccess: auth.publicAccess,\n allowedInstallationOwners: normalizedOrgs,\n },\n ];\n providerKey = `${connection.host}:app:${appId}:${JSON.stringify(\n normalizedOrgs ?? [],\n )}:${String(auth.publicAccess ?? false)}`;\n } else if (auth.method === 'token') {\n config.token = auth.token;\n } else {\n providerKey = `${connection.host}:none`;\n }\n\n let provider = providerKey ? this.providers.get(providerKey) : undefined;\n if (!provider) {\n provider = SingleInstanceGithubCredentialsProvider.create(config);\n if (providerKey) {\n this.providers.set(providerKey, provider);\n }\n }\n\n return provider.getCredentials(opts);\n }\n\n const parsed = new URL(opts.url);\n const provider = this.providers.get(parsed.host);\n\n if (!provider) {\n throw new Error(\n `There is no GitHub integration that matches ${opts.url}. Please add a configuration for an integration.`,\n );\n }\n\n return provider.getCredentials(opts);\n }\n}\n"],"names":["SingleInstanceGithubCredentialsProvider","ForwardedError","InputError","provider"],"mappings":"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"}
@@ -56,7 +56,7 @@ class DefaultGithubCredentialsProvider {
56
56
  if (this.#connections) {
57
57
  const connection = await this.#connections.find({
58
58
  type: "github",
59
- url: opts.url,
59
+ query: { url: opts.url },
60
60
  authMethods: ["app", "token", "none"]
61
61
  }).catch((error) => {
62
62
  throw new ForwardedError(
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultGithubCredentialsProvider.esm.js","sources":["../../src/github/DefaultGithubCredentialsProvider.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { GithubCredentials, GithubCredentialsProvider } from './types';\nimport { ScmIntegrationRegistry } from '../registry';\nimport { SingleInstanceGithubCredentialsProvider } from './SingleInstanceGithubCredentialsProvider';\nimport type { ConnectionsService } from '@backstage/connections';\nimport { ForwardedError, InputError } from '@backstage/errors';\nimport type { GithubIntegrationConfig } from './config';\n\n/**\n * Handles the creation and caching of credentials for GitHub integrations.\n *\n * @public\n * @remarks\n *\n * TODO: Possibly move this to a backend only package so that it's not used in the frontend by mistake\n */\nexport class DefaultGithubCredentialsProvider\n implements GithubCredentialsProvider\n{\n static fromIntegrations(integrations: ScmIntegrationRegistry) {\n const credentialsProviders: Map<string, GithubCredentialsProvider> =\n new Map<string, GithubCredentialsProvider>();\n\n integrations.github.list().forEach(integration => {\n const credentialsProvider =\n SingleInstanceGithubCredentialsProvider.create(integration.config);\n credentialsProviders.set(integration.config.host, credentialsProvider);\n });\n return new DefaultGithubCredentialsProvider(credentialsProviders);\n }\n\n /**\n * Creates a credentials provider backed by the connections service.\n *\n * @param connections - The connections service used to resolve GitHub credentials.\n * @internal\n */\n static experimentalFromConnections(connections: ConnectionsService) {\n return new DefaultGithubCredentialsProvider(\n new Map<string, GithubCredentialsProvider>(),\n connections,\n );\n }\n\n private readonly providers: Map<string, GithubCredentialsProvider>;\n readonly #connections?: ConnectionsService;\n\n private constructor(\n providers: Map<string, GithubCredentialsProvider>,\n connections?: ConnectionsService,\n ) {\n this.providers = providers;\n this.#connections = connections;\n }\n\n /**\n * Returns {@link GithubCredentials} for a given URL.\n *\n * @remarks\n *\n * Consecutive calls to this method with the same URL will return cached\n * credentials.\n *\n * The shortest lifetime for a token returned is 10 minutes.\n *\n * @example\n * ```ts\n * const { token, headers } = await getCredentials({\n * url: 'https://github.com/backstage/foobar'\n * })\n *\n * const { token, headers } = await getCredentials({\n * url: 'https://github.com/backstage'\n * })\n * ```\n *\n * @param opts - The organization or repository URL\n * @returns A promise of {@link GithubCredentials}.\n */\n async getCredentials(opts: { url: string }): Promise<GithubCredentials> {\n if (this.#connections) {\n // Ask the connections service to select auth for this URL. A host may\n // have different GitHub Apps for different organizations, so this\n // selection cannot be done once when the provider is created.\n\n const connection = await this.#connections\n .find({\n type: 'github',\n url: opts.url,\n authMethods: ['app', 'token', 'none'],\n })\n .catch(error => {\n throw new ForwardedError(\n 'Failed getting credentials from connection',\n error,\n );\n });\n\n const { auth } = connection;\n\n // Adapt the connection schema to the existing provider configuration so\n // credential creation and token caching stay in one implementation.\n const config: GithubIntegrationConfig = {\n host: connection.host,\n apiBaseUrl: connection.apiBaseUrl,\n rawBaseUrl: connection.rawBaseUrl,\n };\n\n // Reusing an App provider preserves its installation-token cache. App\n // organizations are canonicalized because GitHub owner matching is\n // case-insensitive and does not depend on ordering. Static token\n // providers have no internal token cache, so they are recreated rather\n // than placing their secret token in a cache key.\n let providerKey: string | undefined;\n if (auth.method === 'app') {\n const appId = Number(auth.appId);\n if (!Number.isSafeInteger(appId) || appId <= 0) {\n throw new InputError(\n `Invalid GitHub App ID \"${auth.appId}\", expected a positive safe integer`,\n );\n }\n const normalizedOrgs = auth.orgs?.length\n ? Array.from(\n new Set(auth.orgs.map(org => org.toLocaleLowerCase('en-US'))),\n ).sort()\n : undefined;\n config.apps = [\n {\n appId,\n privateKey: auth.privateKey,\n clientId: auth.clientId,\n clientSecret: auth.clientSecret,\n webhookSecret: auth.webhookSecret,\n publicAccess: auth.publicAccess,\n allowedInstallationOwners: normalizedOrgs,\n },\n ];\n providerKey = `${connection.host}:app:${appId}:${JSON.stringify(\n normalizedOrgs ?? [],\n )}:${String(auth.publicAccess ?? false)}`;\n } else if (auth.method === 'token') {\n config.token = auth.token;\n } else {\n providerKey = `${connection.host}:none`;\n }\n\n let provider = providerKey ? this.providers.get(providerKey) : undefined;\n if (!provider) {\n provider = SingleInstanceGithubCredentialsProvider.create(config);\n if (providerKey) {\n this.providers.set(providerKey, provider);\n }\n }\n\n return provider.getCredentials(opts);\n }\n\n const parsed = new URL(opts.url);\n const provider = this.providers.get(parsed.host);\n\n if (!provider) {\n throw new Error(\n `There is no GitHub integration that matches ${opts.url}. Please add a configuration for an integration.`,\n );\n }\n\n return provider.getCredentials(opts);\n }\n}\n"],"names":["provider"],"mappings":"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"}
1
+ {"version":3,"file":"DefaultGithubCredentialsProvider.esm.js","sources":["../../src/github/DefaultGithubCredentialsProvider.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { GithubCredentials, GithubCredentialsProvider } from './types';\nimport { ScmIntegrationRegistry } from '../registry';\nimport { SingleInstanceGithubCredentialsProvider } from './SingleInstanceGithubCredentialsProvider';\nimport type { ConnectionsService } from '@backstage/connections';\nimport { ForwardedError, InputError } from '@backstage/errors';\nimport type { GithubIntegrationConfig } from './config';\n\n/**\n * Handles the creation and caching of credentials for GitHub integrations.\n *\n * @public\n * @remarks\n *\n * TODO: Possibly move this to a backend only package so that it's not used in the frontend by mistake\n */\nexport class DefaultGithubCredentialsProvider\n implements GithubCredentialsProvider\n{\n static fromIntegrations(integrations: ScmIntegrationRegistry) {\n const credentialsProviders: Map<string, GithubCredentialsProvider> =\n new Map<string, GithubCredentialsProvider>();\n\n integrations.github.list().forEach(integration => {\n const credentialsProvider =\n SingleInstanceGithubCredentialsProvider.create(integration.config);\n credentialsProviders.set(integration.config.host, credentialsProvider);\n });\n return new DefaultGithubCredentialsProvider(credentialsProviders);\n }\n\n /**\n * Creates a credentials provider backed by the connections service.\n *\n * @param connections - The connections service used to resolve GitHub credentials.\n * @internal\n */\n static experimentalFromConnections(connections: ConnectionsService) {\n return new DefaultGithubCredentialsProvider(\n new Map<string, GithubCredentialsProvider>(),\n connections,\n );\n }\n\n private readonly providers: Map<string, GithubCredentialsProvider>;\n readonly #connections?: ConnectionsService;\n\n private constructor(\n providers: Map<string, GithubCredentialsProvider>,\n connections?: ConnectionsService,\n ) {\n this.providers = providers;\n this.#connections = connections;\n }\n\n /**\n * Returns {@link GithubCredentials} for a given URL.\n *\n * @remarks\n *\n * Consecutive calls to this method with the same URL will return cached\n * credentials.\n *\n * The shortest lifetime for a token returned is 10 minutes.\n *\n * @example\n * ```ts\n * const { token, headers } = await getCredentials({\n * url: 'https://github.com/backstage/foobar'\n * })\n *\n * const { token, headers } = await getCredentials({\n * url: 'https://github.com/backstage'\n * })\n * ```\n *\n * @param opts - The organization or repository URL\n * @returns A promise of {@link GithubCredentials}.\n */\n async getCredentials(opts: { url: string }): Promise<GithubCredentials> {\n if (this.#connections) {\n // Ask the connections service to select auth for this URL. A host may\n // have different GitHub Apps for different organizations, so this\n // selection cannot be done once when the provider is created.\n\n const connection = await this.#connections\n .find({\n type: 'github',\n query: { url: opts.url },\n authMethods: ['app', 'token', 'none'],\n })\n .catch(error => {\n throw new ForwardedError(\n 'Failed getting credentials from connection',\n error,\n );\n });\n\n const { auth } = connection;\n\n // Adapt the connection schema to the existing provider configuration so\n // credential creation and token caching stay in one implementation.\n const config: GithubIntegrationConfig = {\n host: connection.host,\n apiBaseUrl: connection.apiBaseUrl,\n rawBaseUrl: connection.rawBaseUrl,\n };\n\n // Reusing an App provider preserves its installation-token cache. App\n // organizations are canonicalized because GitHub owner matching is\n // case-insensitive and does not depend on ordering. Static token\n // providers have no internal token cache, so they are recreated rather\n // than placing their secret token in a cache key.\n let providerKey: string | undefined;\n if (auth.method === 'app') {\n const appId = Number(auth.appId);\n if (!Number.isSafeInteger(appId) || appId <= 0) {\n throw new InputError(\n `Invalid GitHub App ID \"${auth.appId}\", expected a positive safe integer`,\n );\n }\n const normalizedOrgs = auth.orgs?.length\n ? Array.from(\n new Set(auth.orgs.map(org => org.toLocaleLowerCase('en-US'))),\n ).sort()\n : undefined;\n config.apps = [\n {\n appId,\n privateKey: auth.privateKey,\n clientId: auth.clientId,\n clientSecret: auth.clientSecret,\n webhookSecret: auth.webhookSecret,\n publicAccess: auth.publicAccess,\n allowedInstallationOwners: normalizedOrgs,\n },\n ];\n providerKey = `${connection.host}:app:${appId}:${JSON.stringify(\n normalizedOrgs ?? [],\n )}:${String(auth.publicAccess ?? false)}`;\n } else if (auth.method === 'token') {\n config.token = auth.token;\n } else {\n providerKey = `${connection.host}:none`;\n }\n\n let provider = providerKey ? this.providers.get(providerKey) : undefined;\n if (!provider) {\n provider = SingleInstanceGithubCredentialsProvider.create(config);\n if (providerKey) {\n this.providers.set(providerKey, provider);\n }\n }\n\n return provider.getCredentials(opts);\n }\n\n const parsed = new URL(opts.url);\n const provider = this.providers.get(parsed.host);\n\n if (!provider) {\n throw new Error(\n `There is no GitHub integration that matches ${opts.url}. Please add a configuration for an integration.`,\n );\n }\n\n return provider.getCredentials(opts);\n }\n}\n"],"names":["provider"],"mappings":"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"}
@@ -4,6 +4,7 @@ var parseGitUrl = require('git-url-parse');
4
4
  var authApp = require('@octokit/auth-app');
5
5
  var rest = require('@octokit/rest');
6
6
  var luxon = require('luxon');
7
+ var lodash = require('lodash');
7
8
 
8
9
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
9
10
 
@@ -39,6 +40,15 @@ class Cache {
39
40
  const HEADERS = {
40
41
  Accept: "application/vnd.github.machine-man-preview+json"
41
42
  };
43
+ const INSTALLATIONS_CACHE_TTL_MINUTES = 10;
44
+ const INSTALLATIONS_REFRESH_THROTTLE_SECONDS = 60;
45
+ function isStaleInstallationError(error) {
46
+ if (typeof error !== "object" || error === null) {
47
+ return false;
48
+ }
49
+ const status = error.status;
50
+ return status === 404 || status === 410;
51
+ }
42
52
  class GithubAppManager {
43
53
  appClient;
44
54
  baseUrl;
@@ -46,6 +56,9 @@ class GithubAppManager {
46
56
  cache = new Cache();
47
57
  allowedInstallationOwners;
48
58
  // undefined allows all installations
59
+ installationsCache;
60
+ lastInstallationsRefreshAttempt;
61
+ pendingInstallations;
49
62
  publicAccess;
50
63
  constructor(config, baseUrl) {
51
64
  this.allowedInstallationOwners = config.allowedInstallationOwners?.map(
@@ -87,10 +100,7 @@ class GithubAppManager {
87
100
  if (suspended) {
88
101
  throw new Error(`The GitHub application for ${owner} is suspended`);
89
102
  }
90
- const result = await this.appClient.apps.createInstallationAccessToken({
91
- installation_id: installationId,
92
- headers: HEADERS
93
- });
103
+ const result = await this.createInstallationAccessToken(installationId);
94
104
  let repositoryNames;
95
105
  if (result.data.repository_selection === "selected") {
96
106
  const installationClient = new rest.Octokit({
@@ -111,7 +121,7 @@ class GithubAppManager {
111
121
  });
112
122
  }
113
123
  async getPublicInstallationToken() {
114
- const [installation] = await this.getInstallations();
124
+ const [installation] = await this.getCachedInstallations();
115
125
  if (!installation) {
116
126
  throw new Error(`No installation found for public app`);
117
127
  }
@@ -119,10 +129,9 @@ class GithubAppManager {
119
129
  `public:${installation.id}`,
120
130
  void 0,
121
131
  async () => {
122
- const result = await this.appClient.apps.createInstallationAccessToken({
123
- installation_id: installation.id,
124
- headers: HEADERS
125
- });
132
+ const result = await this.createInstallationAccessToken(
133
+ installation.id
134
+ );
126
135
  return {
127
136
  token: result.data.token,
128
137
  expiresAt: luxon.DateTime.fromISO(result.data.expires_at)
@@ -130,14 +139,58 @@ class GithubAppManager {
130
139
  }
131
140
  );
132
141
  }
133
- getInstallations() {
134
- return this.appClient.paginate(this.appClient.apps.listInstallations);
142
+ async createInstallationAccessToken(installationId) {
143
+ try {
144
+ return await this.appClient.apps.createInstallationAccessToken({
145
+ installation_id: installationId,
146
+ headers: HEADERS
147
+ });
148
+ } catch (error) {
149
+ if (isStaleInstallationError(error)) {
150
+ this.installationsCache = void 0;
151
+ }
152
+ throw error;
153
+ }
154
+ }
155
+ async getInstallations() {
156
+ return lodash.cloneDeep(await this.getCachedInstallations());
157
+ }
158
+ async getCachedInstallations(options = {}) {
159
+ if (!options.forceRefresh && this.installationsCache && luxon.DateTime.local() < this.installationsCache.expiresAt) {
160
+ return this.installationsCache.data;
161
+ }
162
+ if (!this.pendingInstallations) {
163
+ const pending = this.appClient.paginate(this.appClient.apps.listInstallations).then((data) => {
164
+ const now = luxon.DateTime.local();
165
+ this.installationsCache = {
166
+ data,
167
+ fetchedAt: now,
168
+ expiresAt: now.plus({ minutes: INSTALLATIONS_CACHE_TTL_MINUTES })
169
+ };
170
+ return data;
171
+ }).finally(() => {
172
+ this.lastInstallationsRefreshAttempt = luxon.DateTime.local();
173
+ if (this.pendingInstallations === pending) {
174
+ this.pendingInstallations = void 0;
175
+ }
176
+ });
177
+ this.pendingInstallations = pending;
178
+ }
179
+ return await this.pendingInstallations;
135
180
  }
136
181
  async getInstallationData(owner) {
137
- const allInstallations = await this.getInstallations();
138
- const installation = allInstallations.find(
139
- (inst) => inst.account && "login" in inst.account && inst.account.login?.toLocaleLowerCase("en-US") === owner.toLocaleLowerCase("en-US")
182
+ const ownerLower = owner.toLocaleLowerCase("en-US");
183
+ const find = (list) => list.find(
184
+ (inst) => inst.account && "login" in inst.account && inst.account.login?.toLocaleLowerCase("en-US") === ownerLower
140
185
  );
186
+ let installations = await this.getCachedInstallations();
187
+ let installation = find(installations);
188
+ if (!installation && this.canRefreshInstallations()) {
189
+ installations = await this.getCachedInstallations({
190
+ forceRefresh: true
191
+ });
192
+ installation = find(installations);
193
+ }
141
194
  if (installation) {
142
195
  return {
143
196
  installationId: installation.id,
@@ -150,6 +203,14 @@ class GithubAppManager {
150
203
  notFoundError.name = "NotFoundError";
151
204
  throw notFoundError;
152
205
  }
206
+ canRefreshInstallations() {
207
+ if (!this.installationsCache) {
208
+ return true;
209
+ }
210
+ const refreshReference = this.lastInstallationsRefreshAttempt ?? this.installationsCache.fetchedAt;
211
+ const age = luxon.DateTime.local().diff(refreshReference).as("seconds");
212
+ return age >= INSTALLATIONS_REFRESH_THROTTLE_SECONDS;
213
+ }
153
214
  }
154
215
  class GithubAppCredentialsMux {
155
216
  apps;
@@ -1 +1 @@
1
- {"version":3,"file":"SingleInstanceGithubCredentialsProvider.cjs.js","sources":["../../src/github/SingleInstanceGithubCredentialsProvider.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport parseGitUrl from 'git-url-parse';\nimport { GithubAppConfig, GithubIntegrationConfig } from './config';\nimport { createAppAuth } from '@octokit/auth-app';\nimport { Octokit, RestEndpointMethodTypes } from '@octokit/rest';\nimport { DateTime } from 'luxon';\nimport {\n GithubCredentials,\n GithubCredentialsProvider,\n GithubCredentialType,\n} from './types';\n\ntype InstallationData = {\n installationId: number;\n suspended: boolean;\n};\n\ntype InstallationTokenData = {\n token: string;\n expiresAt: DateTime;\n repositories?: String[];\n};\n\nclass Cache {\n private readonly tokenCache = new Map<string, InstallationTokenData>();\n\n async getOrCreateToken(\n owner: string,\n repo: string | undefined,\n supplier: () => Promise<InstallationTokenData>,\n ): Promise<{ accessToken: string }> {\n let existingInstallationData = this.tokenCache.get(owner);\n\n if (\n !existingInstallationData ||\n this.isExpired(existingInstallationData.expiresAt)\n ) {\n existingInstallationData = await supplier();\n // Allow 10 minutes grace to account for clock skew\n existingInstallationData.expiresAt =\n existingInstallationData.expiresAt.minus({ minutes: 10 });\n this.tokenCache.set(owner, existingInstallationData);\n }\n\n if (!this.appliesToRepo(existingInstallationData, repo)) {\n throw new Error(\n `The Backstage GitHub application used in the ${owner} organization does not have access to a repository with the name ${repo}`,\n );\n }\n\n return { accessToken: existingInstallationData.token };\n }\n\n private isExpired = (date: DateTime) => DateTime.local() > date;\n\n private appliesToRepo(tokenData: InstallationTokenData, repo?: string) {\n // If no specific repo has been requested the token is applicable\n if (repo === undefined) {\n return true;\n }\n // If the token is restricted to repositories, the token only applies if the repo is in the allow list\n if (tokenData.repositories !== undefined) {\n return tokenData.repositories.includes(repo);\n }\n // Otherwise the token is applicable\n return true;\n }\n}\n\n/**\n * This accept header is required when calling App APIs in GitHub Enterprise.\n * It has no effect on calls to github.com and can probably be removed entirely\n * once GitHub Apps is out of preview.\n */\nconst HEADERS = {\n Accept: 'application/vnd.github.machine-man-preview+json',\n};\n\n/**\n * GithubAppManager issues and caches tokens for a specific GitHub App.\n */\nclass GithubAppManager {\n private readonly appClient: Octokit;\n private readonly baseUrl?: string;\n private readonly baseAuthConfig: { appId: number; privateKey: string };\n private readonly cache = new Cache();\n private readonly allowedInstallationOwners: string[] | undefined; // undefined allows all installations\n public readonly publicAccess: boolean;\n\n constructor(config: GithubAppConfig, baseUrl?: string) {\n this.allowedInstallationOwners = config.allowedInstallationOwners?.map(\n owner => owner.toLocaleLowerCase('en-US'),\n );\n this.baseUrl = baseUrl;\n this.baseAuthConfig = {\n appId: config.appId,\n privateKey: config.privateKey.replace(/\\\\n/gm, '\\n'),\n };\n this.appClient = new Octokit({\n baseUrl,\n headers: HEADERS,\n authStrategy: createAppAuth,\n auth: this.baseAuthConfig,\n });\n this.publicAccess = config.publicAccess ?? false;\n }\n\n async getInstallationCredentials(\n owner?: string,\n repo?: string,\n ): Promise<{ accessToken: string | undefined }> {\n // No owner means a bare host URL (e.g. https://github.com) — return an\n // app-level JWT rather than an installation token.\n if (!owner) {\n const auth = createAppAuth({\n appId: this.baseAuthConfig.appId,\n privateKey: this.baseAuthConfig.privateKey,\n });\n const { token } = await auth({ type: 'app' });\n return { accessToken: token };\n }\n\n if (this.allowedInstallationOwners) {\n if (\n !this.allowedInstallationOwners?.includes(\n owner.toLocaleLowerCase('en-US'),\n )\n ) {\n return { accessToken: undefined }; // An empty token allows anonymous access to public repos\n }\n }\n\n // Go and grab an access token for the app scoped to a repository if provided, if not use the organisation installation.\n return this.cache.getOrCreateToken(owner, repo, async () => {\n const { installationId, suspended } = await this.getInstallationData(\n owner,\n );\n if (suspended) {\n throw new Error(`The GitHub application for ${owner} is suspended`);\n }\n\n const result = await this.appClient.apps.createInstallationAccessToken({\n installation_id: installationId,\n headers: HEADERS,\n });\n\n let repositoryNames;\n\n if (result.data.repository_selection === 'selected') {\n const installationClient = new Octokit({\n baseUrl: this.baseUrl,\n auth: result.data.token,\n });\n const repos = await installationClient.paginate(\n installationClient.apps.listReposAccessibleToInstallation,\n );\n // The return type of the paginate method is incorrect.\n const repositories: RestEndpointMethodTypes['apps']['listReposAccessibleToInstallation']['response']['data']['repositories'] =\n repos.repositories ?? repos;\n\n repositoryNames = repositories.map(repository => repository.name);\n }\n return {\n token: result.data.token,\n expiresAt: DateTime.fromISO(result.data.expires_at),\n repositories: repositoryNames,\n };\n });\n }\n\n async getPublicInstallationToken(): Promise<{ accessToken: string }> {\n const [installation] = await this.getInstallations();\n\n if (!installation) {\n throw new Error(`No installation found for public app`);\n }\n\n return this.cache.getOrCreateToken(\n `public:${installation.id}`,\n undefined,\n async () => {\n const result = await this.appClient.apps.createInstallationAccessToken({\n installation_id: installation.id,\n headers: HEADERS,\n });\n\n return {\n token: result.data.token,\n expiresAt: DateTime.fromISO(result.data.expires_at),\n };\n },\n );\n }\n\n getInstallations(): Promise<\n RestEndpointMethodTypes['apps']['listInstallations']['response']['data']\n > {\n return this.appClient.paginate(this.appClient.apps.listInstallations);\n }\n\n private async getInstallationData(owner: string): Promise<InstallationData> {\n const allInstallations = await this.getInstallations();\n const installation = allInstallations.find(\n inst =>\n inst.account &&\n 'login' in inst.account &&\n inst.account.login?.toLocaleLowerCase('en-US') ===\n owner.toLocaleLowerCase('en-US'),\n );\n\n if (installation) {\n return {\n installationId: installation.id,\n suspended: Boolean(installation.suspended_by),\n };\n }\n\n const notFoundError = new Error(\n `No app installation found for ${owner} in ${this.baseAuthConfig.appId}`,\n );\n notFoundError.name = 'NotFoundError';\n throw notFoundError;\n }\n}\n\n/**\n * Corresponds to a Github installation which internally could hold several GitHub Apps.\n *\n * @public\n */\nexport class GithubAppCredentialsMux {\n private readonly apps: GithubAppManager[];\n\n constructor(config: GithubIntegrationConfig, appIds: number[] = []) {\n this.apps =\n config.apps\n ?.filter(app => (appIds.length ? appIds.includes(app.appId) : true))\n .map(ac => new GithubAppManager(ac, config.apiBaseUrl)) ?? [];\n }\n\n async getAllInstallations(): Promise<\n RestEndpointMethodTypes['apps']['listInstallations']['response']['data']\n > {\n if (!this.apps.length) {\n return [];\n }\n\n const installs = await Promise.all(\n this.apps.map(app => app.getInstallations()),\n );\n\n return installs.flat();\n }\n\n async getAppToken(\n owner?: string,\n repo?: string,\n ): Promise<string | undefined> {\n if (this.apps.length === 0) {\n return undefined;\n }\n\n const results = await Promise.all(\n this.apps.map(app =>\n app.getInstallationCredentials(owner, repo).then(\n credentials => ({ credentials, error: undefined }),\n error => ({ credentials: undefined, error }),\n ),\n ),\n );\n\n const result = results.find(\n resultItem => resultItem.credentials?.accessToken,\n );\n\n if (result) {\n return result.credentials!.accessToken;\n }\n\n // If there was no token returned, then let's find a public access app and use an installation to get a token.\n const publicAccessApp = this.apps.find(app => app.publicAccess);\n if (publicAccessApp) {\n const publicResult = await publicAccessApp\n .getPublicInstallationToken()\n .then(\n credentials => ({ credentials, error: undefined }),\n error => ({ credentials: undefined, error }),\n );\n\n if (publicResult.credentials?.accessToken) {\n return publicResult.credentials.accessToken;\n }\n }\n\n const errors = results.map(r => r.error);\n const notNotFoundError = errors.find(err => err?.name !== 'NotFoundError');\n if (notNotFoundError) {\n throw notNotFoundError;\n }\n\n return undefined;\n }\n}\n\n/**\n * Handles the creation and caching of credentials for GitHub integrations.\n *\n * @public\n * @remarks\n *\n * TODO: Possibly move this to a backend only package so that it's not used in the frontend by mistake\n */\nexport class SingleInstanceGithubCredentialsProvider\n implements GithubCredentialsProvider\n{\n static create: (\n config: GithubIntegrationConfig,\n ) => GithubCredentialsProvider = config => {\n return new SingleInstanceGithubCredentialsProvider(\n new GithubAppCredentialsMux(config),\n config.token,\n );\n };\n\n private readonly githubAppCredentialsMux: GithubAppCredentialsMux;\n private readonly token?: string;\n\n private constructor(\n githubAppCredentialsMux: GithubAppCredentialsMux,\n token?: string,\n ) {\n this.githubAppCredentialsMux = githubAppCredentialsMux;\n this.token = token;\n }\n\n /**\n * Returns {@link GithubCredentials} for a given URL.\n *\n * @remarks\n *\n * Consecutive calls to this method with the same URL will return cached\n * credentials.\n *\n * The shortest lifetime for a token returned is 10 minutes.\n *\n * @example\n * ```ts\n * const { token, headers } = await getCredentials({\n * url: 'github.com/backstage/foobar'\n * })\n * ```\n *\n * @param opts - The organization or repository URL\n * @returns A promise of {@link GithubCredentials}.\n */\n async getCredentials(opts: { url: string }): Promise<GithubCredentials> {\n const parsed = parseGitUrl(opts.url);\n\n const owner = parsed.owner || parsed.name;\n const repo = parsed.owner ? parsed.name : undefined;\n\n let type: GithubCredentialType = 'app';\n let token = await this.githubAppCredentialsMux.getAppToken(owner, repo);\n if (!token) {\n type = 'token';\n token = this.token;\n }\n\n return {\n headers: token ? { Authorization: `Bearer ${token}` } : undefined,\n token,\n type,\n };\n }\n}\n"],"names":["DateTime","Octokit","createAppAuth","parseGitUrl"],"mappings":"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"}
1
+ {"version":3,"file":"SingleInstanceGithubCredentialsProvider.cjs.js","sources":["../../src/github/SingleInstanceGithubCredentialsProvider.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport parseGitUrl from 'git-url-parse';\nimport { GithubAppConfig, GithubIntegrationConfig } from './config';\nimport { createAppAuth } from '@octokit/auth-app';\nimport { Octokit, RestEndpointMethodTypes } from '@octokit/rest';\nimport { DateTime } from 'luxon';\nimport { cloneDeep } from 'lodash';\nimport {\n GithubCredentials,\n GithubCredentialsProvider,\n GithubCredentialType,\n} from './types';\n\ntype InstallationData = {\n installationId: number;\n suspended: boolean;\n};\n\ntype InstallationTokenData = {\n token: string;\n expiresAt: DateTime;\n repositories?: String[];\n};\n\nclass Cache {\n private readonly tokenCache = new Map<string, InstallationTokenData>();\n\n async getOrCreateToken(\n owner: string,\n repo: string | undefined,\n supplier: () => Promise<InstallationTokenData>,\n ): Promise<{ accessToken: string }> {\n let existingInstallationData = this.tokenCache.get(owner);\n\n if (\n !existingInstallationData ||\n this.isExpired(existingInstallationData.expiresAt)\n ) {\n existingInstallationData = await supplier();\n // Allow 10 minutes grace to account for clock skew\n existingInstallationData.expiresAt =\n existingInstallationData.expiresAt.minus({ minutes: 10 });\n this.tokenCache.set(owner, existingInstallationData);\n }\n\n if (!this.appliesToRepo(existingInstallationData, repo)) {\n throw new Error(\n `The Backstage GitHub application used in the ${owner} organization does not have access to a repository with the name ${repo}`,\n );\n }\n\n return { accessToken: existingInstallationData.token };\n }\n\n private isExpired = (date: DateTime) => DateTime.local() > date;\n\n private appliesToRepo(tokenData: InstallationTokenData, repo?: string) {\n // If no specific repo has been requested the token is applicable\n if (repo === undefined) {\n return true;\n }\n // If the token is restricted to repositories, the token only applies if the repo is in the allow list\n if (tokenData.repositories !== undefined) {\n return tokenData.repositories.includes(repo);\n }\n // Otherwise the token is applicable\n return true;\n }\n}\n\n/**\n * This accept header is required when calling App APIs in GitHub Enterprise.\n * It has no effect on calls to github.com and can probably be removed entirely\n * once GitHub Apps is out of preview.\n */\nconst HEADERS = {\n Accept: 'application/vnd.github.machine-man-preview+json',\n};\n\ntype Installations =\n RestEndpointMethodTypes['apps']['listInstallations']['response']['data'];\n\n// How long a listInstallations response may be reused before refetching.\n// Short enough that newly-added installations show up quickly, long enough\n// that token refresh cycles don't re-paginate on every miss.\nconst INSTALLATIONS_CACHE_TTL_MINUTES = 10;\n\n// Minimum time between on-demand refreshes triggered by an owner miss. This\n// keeps the cache from being paginated on every lookup for an unknown owner,\n// while still letting a newly-added installation show up before the TTL.\nconst INSTALLATIONS_REFRESH_THROTTLE_SECONDS = 60;\n\nfunction isStaleInstallationError(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) {\n return false;\n }\n const status = (error as { status?: unknown }).status;\n return status === 404 || status === 410;\n}\n\n/**\n * GithubAppManager issues and caches tokens for a specific GitHub App.\n */\nclass GithubAppManager {\n private readonly appClient: Octokit;\n private readonly baseUrl?: string;\n private readonly baseAuthConfig: { appId: number; privateKey: string };\n private readonly cache = new Cache();\n private readonly allowedInstallationOwners: string[] | undefined; // undefined allows all installations\n private installationsCache?: {\n data: Installations;\n fetchedAt: DateTime;\n expiresAt: DateTime;\n };\n private lastInstallationsRefreshAttempt?: DateTime;\n private pendingInstallations?: Promise<Installations>;\n public readonly publicAccess: boolean;\n\n constructor(config: GithubAppConfig, baseUrl?: string) {\n this.allowedInstallationOwners = config.allowedInstallationOwners?.map(\n owner => owner.toLocaleLowerCase('en-US'),\n );\n this.baseUrl = baseUrl;\n this.baseAuthConfig = {\n appId: config.appId,\n privateKey: config.privateKey.replace(/\\\\n/gm, '\\n'),\n };\n this.appClient = new Octokit({\n baseUrl,\n headers: HEADERS,\n authStrategy: createAppAuth,\n auth: this.baseAuthConfig,\n });\n this.publicAccess = config.publicAccess ?? false;\n }\n\n async getInstallationCredentials(\n owner?: string,\n repo?: string,\n ): Promise<{ accessToken: string | undefined }> {\n // No owner means a bare host URL (e.g. https://github.com) — return an\n // app-level JWT rather than an installation token.\n if (!owner) {\n const auth = createAppAuth({\n appId: this.baseAuthConfig.appId,\n privateKey: this.baseAuthConfig.privateKey,\n });\n const { token } = await auth({ type: 'app' });\n return { accessToken: token };\n }\n\n if (this.allowedInstallationOwners) {\n if (\n !this.allowedInstallationOwners?.includes(\n owner.toLocaleLowerCase('en-US'),\n )\n ) {\n return { accessToken: undefined }; // An empty token allows anonymous access to public repos\n }\n }\n\n // Go and grab an access token for the app scoped to a repository if provided, if not use the organisation installation.\n return this.cache.getOrCreateToken(owner, repo, async () => {\n const { installationId, suspended } = await this.getInstallationData(\n owner,\n );\n if (suspended) {\n throw new Error(`The GitHub application for ${owner} is suspended`);\n }\n\n const result = await this.createInstallationAccessToken(installationId);\n\n let repositoryNames;\n\n if (result.data.repository_selection === 'selected') {\n const installationClient = new Octokit({\n baseUrl: this.baseUrl,\n auth: result.data.token,\n });\n const repos = await installationClient.paginate(\n installationClient.apps.listReposAccessibleToInstallation,\n );\n // The return type of the paginate method is incorrect.\n const repositories: RestEndpointMethodTypes['apps']['listReposAccessibleToInstallation']['response']['data']['repositories'] =\n repos.repositories ?? repos;\n\n repositoryNames = repositories.map(repository => repository.name);\n }\n return {\n token: result.data.token,\n expiresAt: DateTime.fromISO(result.data.expires_at),\n repositories: repositoryNames,\n };\n });\n }\n\n async getPublicInstallationToken(): Promise<{ accessToken: string }> {\n const [installation] = await this.getCachedInstallations();\n\n if (!installation) {\n throw new Error(`No installation found for public app`);\n }\n\n return this.cache.getOrCreateToken(\n `public:${installation.id}`,\n undefined,\n async () => {\n const result = await this.createInstallationAccessToken(\n installation.id,\n );\n\n return {\n token: result.data.token,\n expiresAt: DateTime.fromISO(result.data.expires_at),\n };\n },\n );\n }\n\n private async createInstallationAccessToken(installationId: number) {\n try {\n return await this.appClient.apps.createInstallationAccessToken({\n installation_id: installationId,\n headers: HEADERS,\n });\n } catch (error) {\n // A 404/410 means the installation referenced in our cache no longer\n // exists, so drop the cache to force a refresh on the next lookup.\n if (isStaleInstallationError(error)) {\n this.installationsCache = undefined;\n }\n throw error;\n }\n }\n\n async getInstallations(): Promise<Installations> {\n return cloneDeep(await this.getCachedInstallations());\n }\n\n private async getCachedInstallations(\n options: { forceRefresh?: boolean } = {},\n ): Promise<Installations> {\n if (\n !options.forceRefresh &&\n this.installationsCache &&\n DateTime.local() < this.installationsCache.expiresAt\n ) {\n return this.installationsCache.data;\n }\n if (!this.pendingInstallations) {\n const pending = this.appClient\n .paginate(this.appClient.apps.listInstallations)\n .then(data => {\n const now = DateTime.local();\n this.installationsCache = {\n data,\n fetchedAt: now,\n expiresAt: now.plus({ minutes: INSTALLATIONS_CACHE_TTL_MINUTES }),\n };\n return data;\n })\n .finally(() => {\n this.lastInstallationsRefreshAttempt = DateTime.local();\n if (this.pendingInstallations === pending) {\n this.pendingInstallations = undefined;\n }\n });\n this.pendingInstallations = pending;\n }\n return await this.pendingInstallations;\n }\n\n private async getInstallationData(owner: string): Promise<InstallationData> {\n const ownerLower = owner.toLocaleLowerCase('en-US');\n const find = (list: Installations) =>\n list.find(\n inst =>\n inst.account &&\n 'login' in inst.account &&\n inst.account.login?.toLocaleLowerCase('en-US') === ownerLower,\n );\n\n let installations = await this.getCachedInstallations();\n let installation = find(installations);\n\n // Owner not in cache — a newly-created installation may have appeared\n // since we last paginated. Force a refresh (throttled) before failing.\n if (!installation && this.canRefreshInstallations()) {\n installations = await this.getCachedInstallations({\n forceRefresh: true,\n });\n installation = find(installations);\n }\n\n if (installation) {\n return {\n installationId: installation.id,\n suspended: Boolean(installation.suspended_by),\n };\n }\n\n const notFoundError = new Error(\n `No app installation found for ${owner} in ${this.baseAuthConfig.appId}`,\n );\n notFoundError.name = 'NotFoundError';\n throw notFoundError;\n }\n\n private canRefreshInstallations(): boolean {\n if (!this.installationsCache) {\n return true;\n }\n const refreshReference =\n this.lastInstallationsRefreshAttempt ?? this.installationsCache.fetchedAt;\n const age = DateTime.local().diff(refreshReference).as('seconds');\n return age >= INSTALLATIONS_REFRESH_THROTTLE_SECONDS;\n }\n}\n\n/**\n * Corresponds to a Github installation which internally could hold several GitHub Apps.\n *\n * @public\n */\nexport class GithubAppCredentialsMux {\n private readonly apps: GithubAppManager[];\n\n constructor(config: GithubIntegrationConfig, appIds: number[] = []) {\n this.apps =\n config.apps\n ?.filter(app => (appIds.length ? appIds.includes(app.appId) : true))\n .map(ac => new GithubAppManager(ac, config.apiBaseUrl)) ?? [];\n }\n\n async getAllInstallations(): Promise<\n RestEndpointMethodTypes['apps']['listInstallations']['response']['data']\n > {\n if (!this.apps.length) {\n return [];\n }\n\n const installs = await Promise.all(\n this.apps.map(app => app.getInstallations()),\n );\n\n return installs.flat();\n }\n\n async getAppToken(\n owner?: string,\n repo?: string,\n ): Promise<string | undefined> {\n if (this.apps.length === 0) {\n return undefined;\n }\n\n const results = await Promise.all(\n this.apps.map(app =>\n app.getInstallationCredentials(owner, repo).then(\n credentials => ({ credentials, error: undefined }),\n error => ({ credentials: undefined, error }),\n ),\n ),\n );\n\n const result = results.find(\n resultItem => resultItem.credentials?.accessToken,\n );\n\n if (result) {\n return result.credentials!.accessToken;\n }\n\n // If there was no token returned, then let's find a public access app and use an installation to get a token.\n const publicAccessApp = this.apps.find(app => app.publicAccess);\n if (publicAccessApp) {\n const publicResult = await publicAccessApp\n .getPublicInstallationToken()\n .then(\n credentials => ({ credentials, error: undefined }),\n error => ({ credentials: undefined, error }),\n );\n\n if (publicResult.credentials?.accessToken) {\n return publicResult.credentials.accessToken;\n }\n }\n\n const errors = results.map(r => r.error);\n const notNotFoundError = errors.find(err => err?.name !== 'NotFoundError');\n if (notNotFoundError) {\n throw notNotFoundError;\n }\n\n return undefined;\n }\n}\n\n/**\n * Handles the creation and caching of credentials for GitHub integrations.\n *\n * @public\n * @remarks\n *\n * TODO: Possibly move this to a backend only package so that it's not used in the frontend by mistake\n */\nexport class SingleInstanceGithubCredentialsProvider\n implements GithubCredentialsProvider\n{\n static create: (\n config: GithubIntegrationConfig,\n ) => GithubCredentialsProvider = config => {\n return new SingleInstanceGithubCredentialsProvider(\n new GithubAppCredentialsMux(config),\n config.token,\n );\n };\n\n private readonly githubAppCredentialsMux: GithubAppCredentialsMux;\n private readonly token?: string;\n\n private constructor(\n githubAppCredentialsMux: GithubAppCredentialsMux,\n token?: string,\n ) {\n this.githubAppCredentialsMux = githubAppCredentialsMux;\n this.token = token;\n }\n\n /**\n * Returns {@link GithubCredentials} for a given URL.\n *\n * @remarks\n *\n * Consecutive calls to this method with the same URL will return cached\n * credentials.\n *\n * The shortest lifetime for a token returned is 10 minutes.\n *\n * @example\n * ```ts\n * const { token, headers } = await getCredentials({\n * url: 'github.com/backstage/foobar'\n * })\n * ```\n *\n * @param opts - The organization or repository URL\n * @returns A promise of {@link GithubCredentials}.\n */\n async getCredentials(opts: { url: string }): Promise<GithubCredentials> {\n const parsed = parseGitUrl(opts.url);\n\n const owner = parsed.owner || parsed.name;\n const repo = parsed.owner ? parsed.name : undefined;\n\n let type: GithubCredentialType = 'app';\n let token = await this.githubAppCredentialsMux.getAppToken(owner, repo);\n if (!token) {\n type = 'token';\n token = this.token;\n }\n\n return {\n headers: token ? { Authorization: `Bearer ${token}` } : undefined,\n token,\n type,\n };\n }\n}\n"],"names":["DateTime","Octokit","createAppAuth","cloneDeep","parseGitUrl"],"mappings":"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"}
@@ -2,6 +2,7 @@ import parseGitUrl from 'git-url-parse';
2
2
  import { createAppAuth } from '@octokit/auth-app';
3
3
  import { Octokit } from '@octokit/rest';
4
4
  import { DateTime } from 'luxon';
5
+ import { cloneDeep } from 'lodash';
5
6
 
6
7
  class Cache {
7
8
  tokenCache = /* @__PURE__ */ new Map();
@@ -33,6 +34,15 @@ class Cache {
33
34
  const HEADERS = {
34
35
  Accept: "application/vnd.github.machine-man-preview+json"
35
36
  };
37
+ const INSTALLATIONS_CACHE_TTL_MINUTES = 10;
38
+ const INSTALLATIONS_REFRESH_THROTTLE_SECONDS = 60;
39
+ function isStaleInstallationError(error) {
40
+ if (typeof error !== "object" || error === null) {
41
+ return false;
42
+ }
43
+ const status = error.status;
44
+ return status === 404 || status === 410;
45
+ }
36
46
  class GithubAppManager {
37
47
  appClient;
38
48
  baseUrl;
@@ -40,6 +50,9 @@ class GithubAppManager {
40
50
  cache = new Cache();
41
51
  allowedInstallationOwners;
42
52
  // undefined allows all installations
53
+ installationsCache;
54
+ lastInstallationsRefreshAttempt;
55
+ pendingInstallations;
43
56
  publicAccess;
44
57
  constructor(config, baseUrl) {
45
58
  this.allowedInstallationOwners = config.allowedInstallationOwners?.map(
@@ -81,10 +94,7 @@ class GithubAppManager {
81
94
  if (suspended) {
82
95
  throw new Error(`The GitHub application for ${owner} is suspended`);
83
96
  }
84
- const result = await this.appClient.apps.createInstallationAccessToken({
85
- installation_id: installationId,
86
- headers: HEADERS
87
- });
97
+ const result = await this.createInstallationAccessToken(installationId);
88
98
  let repositoryNames;
89
99
  if (result.data.repository_selection === "selected") {
90
100
  const installationClient = new Octokit({
@@ -105,7 +115,7 @@ class GithubAppManager {
105
115
  });
106
116
  }
107
117
  async getPublicInstallationToken() {
108
- const [installation] = await this.getInstallations();
118
+ const [installation] = await this.getCachedInstallations();
109
119
  if (!installation) {
110
120
  throw new Error(`No installation found for public app`);
111
121
  }
@@ -113,10 +123,9 @@ class GithubAppManager {
113
123
  `public:${installation.id}`,
114
124
  void 0,
115
125
  async () => {
116
- const result = await this.appClient.apps.createInstallationAccessToken({
117
- installation_id: installation.id,
118
- headers: HEADERS
119
- });
126
+ const result = await this.createInstallationAccessToken(
127
+ installation.id
128
+ );
120
129
  return {
121
130
  token: result.data.token,
122
131
  expiresAt: DateTime.fromISO(result.data.expires_at)
@@ -124,14 +133,58 @@ class GithubAppManager {
124
133
  }
125
134
  );
126
135
  }
127
- getInstallations() {
128
- return this.appClient.paginate(this.appClient.apps.listInstallations);
136
+ async createInstallationAccessToken(installationId) {
137
+ try {
138
+ return await this.appClient.apps.createInstallationAccessToken({
139
+ installation_id: installationId,
140
+ headers: HEADERS
141
+ });
142
+ } catch (error) {
143
+ if (isStaleInstallationError(error)) {
144
+ this.installationsCache = void 0;
145
+ }
146
+ throw error;
147
+ }
148
+ }
149
+ async getInstallations() {
150
+ return cloneDeep(await this.getCachedInstallations());
151
+ }
152
+ async getCachedInstallations(options = {}) {
153
+ if (!options.forceRefresh && this.installationsCache && DateTime.local() < this.installationsCache.expiresAt) {
154
+ return this.installationsCache.data;
155
+ }
156
+ if (!this.pendingInstallations) {
157
+ const pending = this.appClient.paginate(this.appClient.apps.listInstallations).then((data) => {
158
+ const now = DateTime.local();
159
+ this.installationsCache = {
160
+ data,
161
+ fetchedAt: now,
162
+ expiresAt: now.plus({ minutes: INSTALLATIONS_CACHE_TTL_MINUTES })
163
+ };
164
+ return data;
165
+ }).finally(() => {
166
+ this.lastInstallationsRefreshAttempt = DateTime.local();
167
+ if (this.pendingInstallations === pending) {
168
+ this.pendingInstallations = void 0;
169
+ }
170
+ });
171
+ this.pendingInstallations = pending;
172
+ }
173
+ return await this.pendingInstallations;
129
174
  }
130
175
  async getInstallationData(owner) {
131
- const allInstallations = await this.getInstallations();
132
- const installation = allInstallations.find(
133
- (inst) => inst.account && "login" in inst.account && inst.account.login?.toLocaleLowerCase("en-US") === owner.toLocaleLowerCase("en-US")
176
+ const ownerLower = owner.toLocaleLowerCase("en-US");
177
+ const find = (list) => list.find(
178
+ (inst) => inst.account && "login" in inst.account && inst.account.login?.toLocaleLowerCase("en-US") === ownerLower
134
179
  );
180
+ let installations = await this.getCachedInstallations();
181
+ let installation = find(installations);
182
+ if (!installation && this.canRefreshInstallations()) {
183
+ installations = await this.getCachedInstallations({
184
+ forceRefresh: true
185
+ });
186
+ installation = find(installations);
187
+ }
135
188
  if (installation) {
136
189
  return {
137
190
  installationId: installation.id,
@@ -144,6 +197,14 @@ class GithubAppManager {
144
197
  notFoundError.name = "NotFoundError";
145
198
  throw notFoundError;
146
199
  }
200
+ canRefreshInstallations() {
201
+ if (!this.installationsCache) {
202
+ return true;
203
+ }
204
+ const refreshReference = this.lastInstallationsRefreshAttempt ?? this.installationsCache.fetchedAt;
205
+ const age = DateTime.local().diff(refreshReference).as("seconds");
206
+ return age >= INSTALLATIONS_REFRESH_THROTTLE_SECONDS;
207
+ }
147
208
  }
148
209
  class GithubAppCredentialsMux {
149
210
  apps;
@@ -1 +1 @@
1
- {"version":3,"file":"SingleInstanceGithubCredentialsProvider.esm.js","sources":["../../src/github/SingleInstanceGithubCredentialsProvider.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport parseGitUrl from 'git-url-parse';\nimport { GithubAppConfig, GithubIntegrationConfig } from './config';\nimport { createAppAuth } from '@octokit/auth-app';\nimport { Octokit, RestEndpointMethodTypes } from '@octokit/rest';\nimport { DateTime } from 'luxon';\nimport {\n GithubCredentials,\n GithubCredentialsProvider,\n GithubCredentialType,\n} from './types';\n\ntype InstallationData = {\n installationId: number;\n suspended: boolean;\n};\n\ntype InstallationTokenData = {\n token: string;\n expiresAt: DateTime;\n repositories?: String[];\n};\n\nclass Cache {\n private readonly tokenCache = new Map<string, InstallationTokenData>();\n\n async getOrCreateToken(\n owner: string,\n repo: string | undefined,\n supplier: () => Promise<InstallationTokenData>,\n ): Promise<{ accessToken: string }> {\n let existingInstallationData = this.tokenCache.get(owner);\n\n if (\n !existingInstallationData ||\n this.isExpired(existingInstallationData.expiresAt)\n ) {\n existingInstallationData = await supplier();\n // Allow 10 minutes grace to account for clock skew\n existingInstallationData.expiresAt =\n existingInstallationData.expiresAt.minus({ minutes: 10 });\n this.tokenCache.set(owner, existingInstallationData);\n }\n\n if (!this.appliesToRepo(existingInstallationData, repo)) {\n throw new Error(\n `The Backstage GitHub application used in the ${owner} organization does not have access to a repository with the name ${repo}`,\n );\n }\n\n return { accessToken: existingInstallationData.token };\n }\n\n private isExpired = (date: DateTime) => DateTime.local() > date;\n\n private appliesToRepo(tokenData: InstallationTokenData, repo?: string) {\n // If no specific repo has been requested the token is applicable\n if (repo === undefined) {\n return true;\n }\n // If the token is restricted to repositories, the token only applies if the repo is in the allow list\n if (tokenData.repositories !== undefined) {\n return tokenData.repositories.includes(repo);\n }\n // Otherwise the token is applicable\n return true;\n }\n}\n\n/**\n * This accept header is required when calling App APIs in GitHub Enterprise.\n * It has no effect on calls to github.com and can probably be removed entirely\n * once GitHub Apps is out of preview.\n */\nconst HEADERS = {\n Accept: 'application/vnd.github.machine-man-preview+json',\n};\n\n/**\n * GithubAppManager issues and caches tokens for a specific GitHub App.\n */\nclass GithubAppManager {\n private readonly appClient: Octokit;\n private readonly baseUrl?: string;\n private readonly baseAuthConfig: { appId: number; privateKey: string };\n private readonly cache = new Cache();\n private readonly allowedInstallationOwners: string[] | undefined; // undefined allows all installations\n public readonly publicAccess: boolean;\n\n constructor(config: GithubAppConfig, baseUrl?: string) {\n this.allowedInstallationOwners = config.allowedInstallationOwners?.map(\n owner => owner.toLocaleLowerCase('en-US'),\n );\n this.baseUrl = baseUrl;\n this.baseAuthConfig = {\n appId: config.appId,\n privateKey: config.privateKey.replace(/\\\\n/gm, '\\n'),\n };\n this.appClient = new Octokit({\n baseUrl,\n headers: HEADERS,\n authStrategy: createAppAuth,\n auth: this.baseAuthConfig,\n });\n this.publicAccess = config.publicAccess ?? false;\n }\n\n async getInstallationCredentials(\n owner?: string,\n repo?: string,\n ): Promise<{ accessToken: string | undefined }> {\n // No owner means a bare host URL (e.g. https://github.com) — return an\n // app-level JWT rather than an installation token.\n if (!owner) {\n const auth = createAppAuth({\n appId: this.baseAuthConfig.appId,\n privateKey: this.baseAuthConfig.privateKey,\n });\n const { token } = await auth({ type: 'app' });\n return { accessToken: token };\n }\n\n if (this.allowedInstallationOwners) {\n if (\n !this.allowedInstallationOwners?.includes(\n owner.toLocaleLowerCase('en-US'),\n )\n ) {\n return { accessToken: undefined }; // An empty token allows anonymous access to public repos\n }\n }\n\n // Go and grab an access token for the app scoped to a repository if provided, if not use the organisation installation.\n return this.cache.getOrCreateToken(owner, repo, async () => {\n const { installationId, suspended } = await this.getInstallationData(\n owner,\n );\n if (suspended) {\n throw new Error(`The GitHub application for ${owner} is suspended`);\n }\n\n const result = await this.appClient.apps.createInstallationAccessToken({\n installation_id: installationId,\n headers: HEADERS,\n });\n\n let repositoryNames;\n\n if (result.data.repository_selection === 'selected') {\n const installationClient = new Octokit({\n baseUrl: this.baseUrl,\n auth: result.data.token,\n });\n const repos = await installationClient.paginate(\n installationClient.apps.listReposAccessibleToInstallation,\n );\n // The return type of the paginate method is incorrect.\n const repositories: RestEndpointMethodTypes['apps']['listReposAccessibleToInstallation']['response']['data']['repositories'] =\n repos.repositories ?? repos;\n\n repositoryNames = repositories.map(repository => repository.name);\n }\n return {\n token: result.data.token,\n expiresAt: DateTime.fromISO(result.data.expires_at),\n repositories: repositoryNames,\n };\n });\n }\n\n async getPublicInstallationToken(): Promise<{ accessToken: string }> {\n const [installation] = await this.getInstallations();\n\n if (!installation) {\n throw new Error(`No installation found for public app`);\n }\n\n return this.cache.getOrCreateToken(\n `public:${installation.id}`,\n undefined,\n async () => {\n const result = await this.appClient.apps.createInstallationAccessToken({\n installation_id: installation.id,\n headers: HEADERS,\n });\n\n return {\n token: result.data.token,\n expiresAt: DateTime.fromISO(result.data.expires_at),\n };\n },\n );\n }\n\n getInstallations(): Promise<\n RestEndpointMethodTypes['apps']['listInstallations']['response']['data']\n > {\n return this.appClient.paginate(this.appClient.apps.listInstallations);\n }\n\n private async getInstallationData(owner: string): Promise<InstallationData> {\n const allInstallations = await this.getInstallations();\n const installation = allInstallations.find(\n inst =>\n inst.account &&\n 'login' in inst.account &&\n inst.account.login?.toLocaleLowerCase('en-US') ===\n owner.toLocaleLowerCase('en-US'),\n );\n\n if (installation) {\n return {\n installationId: installation.id,\n suspended: Boolean(installation.suspended_by),\n };\n }\n\n const notFoundError = new Error(\n `No app installation found for ${owner} in ${this.baseAuthConfig.appId}`,\n );\n notFoundError.name = 'NotFoundError';\n throw notFoundError;\n }\n}\n\n/**\n * Corresponds to a Github installation which internally could hold several GitHub Apps.\n *\n * @public\n */\nexport class GithubAppCredentialsMux {\n private readonly apps: GithubAppManager[];\n\n constructor(config: GithubIntegrationConfig, appIds: number[] = []) {\n this.apps =\n config.apps\n ?.filter(app => (appIds.length ? appIds.includes(app.appId) : true))\n .map(ac => new GithubAppManager(ac, config.apiBaseUrl)) ?? [];\n }\n\n async getAllInstallations(): Promise<\n RestEndpointMethodTypes['apps']['listInstallations']['response']['data']\n > {\n if (!this.apps.length) {\n return [];\n }\n\n const installs = await Promise.all(\n this.apps.map(app => app.getInstallations()),\n );\n\n return installs.flat();\n }\n\n async getAppToken(\n owner?: string,\n repo?: string,\n ): Promise<string | undefined> {\n if (this.apps.length === 0) {\n return undefined;\n }\n\n const results = await Promise.all(\n this.apps.map(app =>\n app.getInstallationCredentials(owner, repo).then(\n credentials => ({ credentials, error: undefined }),\n error => ({ credentials: undefined, error }),\n ),\n ),\n );\n\n const result = results.find(\n resultItem => resultItem.credentials?.accessToken,\n );\n\n if (result) {\n return result.credentials!.accessToken;\n }\n\n // If there was no token returned, then let's find a public access app and use an installation to get a token.\n const publicAccessApp = this.apps.find(app => app.publicAccess);\n if (publicAccessApp) {\n const publicResult = await publicAccessApp\n .getPublicInstallationToken()\n .then(\n credentials => ({ credentials, error: undefined }),\n error => ({ credentials: undefined, error }),\n );\n\n if (publicResult.credentials?.accessToken) {\n return publicResult.credentials.accessToken;\n }\n }\n\n const errors = results.map(r => r.error);\n const notNotFoundError = errors.find(err => err?.name !== 'NotFoundError');\n if (notNotFoundError) {\n throw notNotFoundError;\n }\n\n return undefined;\n }\n}\n\n/**\n * Handles the creation and caching of credentials for GitHub integrations.\n *\n * @public\n * @remarks\n *\n * TODO: Possibly move this to a backend only package so that it's not used in the frontend by mistake\n */\nexport class SingleInstanceGithubCredentialsProvider\n implements GithubCredentialsProvider\n{\n static create: (\n config: GithubIntegrationConfig,\n ) => GithubCredentialsProvider = config => {\n return new SingleInstanceGithubCredentialsProvider(\n new GithubAppCredentialsMux(config),\n config.token,\n );\n };\n\n private readonly githubAppCredentialsMux: GithubAppCredentialsMux;\n private readonly token?: string;\n\n private constructor(\n githubAppCredentialsMux: GithubAppCredentialsMux,\n token?: string,\n ) {\n this.githubAppCredentialsMux = githubAppCredentialsMux;\n this.token = token;\n }\n\n /**\n * Returns {@link GithubCredentials} for a given URL.\n *\n * @remarks\n *\n * Consecutive calls to this method with the same URL will return cached\n * credentials.\n *\n * The shortest lifetime for a token returned is 10 minutes.\n *\n * @example\n * ```ts\n * const { token, headers } = await getCredentials({\n * url: 'github.com/backstage/foobar'\n * })\n * ```\n *\n * @param opts - The organization or repository URL\n * @returns A promise of {@link GithubCredentials}.\n */\n async getCredentials(opts: { url: string }): Promise<GithubCredentials> {\n const parsed = parseGitUrl(opts.url);\n\n const owner = parsed.owner || parsed.name;\n const repo = parsed.owner ? parsed.name : undefined;\n\n let type: GithubCredentialType = 'app';\n let token = await this.githubAppCredentialsMux.getAppToken(owner, repo);\n if (!token) {\n type = 'token';\n token = this.token;\n }\n\n return {\n headers: token ? { Authorization: `Bearer ${token}` } : undefined,\n token,\n type,\n };\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SingleInstanceGithubCredentialsProvider.esm.js","sources":["../../src/github/SingleInstanceGithubCredentialsProvider.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport parseGitUrl from 'git-url-parse';\nimport { GithubAppConfig, GithubIntegrationConfig } from './config';\nimport { createAppAuth } from '@octokit/auth-app';\nimport { Octokit, RestEndpointMethodTypes } from '@octokit/rest';\nimport { DateTime } from 'luxon';\nimport { cloneDeep } from 'lodash';\nimport {\n GithubCredentials,\n GithubCredentialsProvider,\n GithubCredentialType,\n} from './types';\n\ntype InstallationData = {\n installationId: number;\n suspended: boolean;\n};\n\ntype InstallationTokenData = {\n token: string;\n expiresAt: DateTime;\n repositories?: String[];\n};\n\nclass Cache {\n private readonly tokenCache = new Map<string, InstallationTokenData>();\n\n async getOrCreateToken(\n owner: string,\n repo: string | undefined,\n supplier: () => Promise<InstallationTokenData>,\n ): Promise<{ accessToken: string }> {\n let existingInstallationData = this.tokenCache.get(owner);\n\n if (\n !existingInstallationData ||\n this.isExpired(existingInstallationData.expiresAt)\n ) {\n existingInstallationData = await supplier();\n // Allow 10 minutes grace to account for clock skew\n existingInstallationData.expiresAt =\n existingInstallationData.expiresAt.minus({ minutes: 10 });\n this.tokenCache.set(owner, existingInstallationData);\n }\n\n if (!this.appliesToRepo(existingInstallationData, repo)) {\n throw new Error(\n `The Backstage GitHub application used in the ${owner} organization does not have access to a repository with the name ${repo}`,\n );\n }\n\n return { accessToken: existingInstallationData.token };\n }\n\n private isExpired = (date: DateTime) => DateTime.local() > date;\n\n private appliesToRepo(tokenData: InstallationTokenData, repo?: string) {\n // If no specific repo has been requested the token is applicable\n if (repo === undefined) {\n return true;\n }\n // If the token is restricted to repositories, the token only applies if the repo is in the allow list\n if (tokenData.repositories !== undefined) {\n return tokenData.repositories.includes(repo);\n }\n // Otherwise the token is applicable\n return true;\n }\n}\n\n/**\n * This accept header is required when calling App APIs in GitHub Enterprise.\n * It has no effect on calls to github.com and can probably be removed entirely\n * once GitHub Apps is out of preview.\n */\nconst HEADERS = {\n Accept: 'application/vnd.github.machine-man-preview+json',\n};\n\ntype Installations =\n RestEndpointMethodTypes['apps']['listInstallations']['response']['data'];\n\n// How long a listInstallations response may be reused before refetching.\n// Short enough that newly-added installations show up quickly, long enough\n// that token refresh cycles don't re-paginate on every miss.\nconst INSTALLATIONS_CACHE_TTL_MINUTES = 10;\n\n// Minimum time between on-demand refreshes triggered by an owner miss. This\n// keeps the cache from being paginated on every lookup for an unknown owner,\n// while still letting a newly-added installation show up before the TTL.\nconst INSTALLATIONS_REFRESH_THROTTLE_SECONDS = 60;\n\nfunction isStaleInstallationError(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) {\n return false;\n }\n const status = (error as { status?: unknown }).status;\n return status === 404 || status === 410;\n}\n\n/**\n * GithubAppManager issues and caches tokens for a specific GitHub App.\n */\nclass GithubAppManager {\n private readonly appClient: Octokit;\n private readonly baseUrl?: string;\n private readonly baseAuthConfig: { appId: number; privateKey: string };\n private readonly cache = new Cache();\n private readonly allowedInstallationOwners: string[] | undefined; // undefined allows all installations\n private installationsCache?: {\n data: Installations;\n fetchedAt: DateTime;\n expiresAt: DateTime;\n };\n private lastInstallationsRefreshAttempt?: DateTime;\n private pendingInstallations?: Promise<Installations>;\n public readonly publicAccess: boolean;\n\n constructor(config: GithubAppConfig, baseUrl?: string) {\n this.allowedInstallationOwners = config.allowedInstallationOwners?.map(\n owner => owner.toLocaleLowerCase('en-US'),\n );\n this.baseUrl = baseUrl;\n this.baseAuthConfig = {\n appId: config.appId,\n privateKey: config.privateKey.replace(/\\\\n/gm, '\\n'),\n };\n this.appClient = new Octokit({\n baseUrl,\n headers: HEADERS,\n authStrategy: createAppAuth,\n auth: this.baseAuthConfig,\n });\n this.publicAccess = config.publicAccess ?? false;\n }\n\n async getInstallationCredentials(\n owner?: string,\n repo?: string,\n ): Promise<{ accessToken: string | undefined }> {\n // No owner means a bare host URL (e.g. https://github.com) — return an\n // app-level JWT rather than an installation token.\n if (!owner) {\n const auth = createAppAuth({\n appId: this.baseAuthConfig.appId,\n privateKey: this.baseAuthConfig.privateKey,\n });\n const { token } = await auth({ type: 'app' });\n return { accessToken: token };\n }\n\n if (this.allowedInstallationOwners) {\n if (\n !this.allowedInstallationOwners?.includes(\n owner.toLocaleLowerCase('en-US'),\n )\n ) {\n return { accessToken: undefined }; // An empty token allows anonymous access to public repos\n }\n }\n\n // Go and grab an access token for the app scoped to a repository if provided, if not use the organisation installation.\n return this.cache.getOrCreateToken(owner, repo, async () => {\n const { installationId, suspended } = await this.getInstallationData(\n owner,\n );\n if (suspended) {\n throw new Error(`The GitHub application for ${owner} is suspended`);\n }\n\n const result = await this.createInstallationAccessToken(installationId);\n\n let repositoryNames;\n\n if (result.data.repository_selection === 'selected') {\n const installationClient = new Octokit({\n baseUrl: this.baseUrl,\n auth: result.data.token,\n });\n const repos = await installationClient.paginate(\n installationClient.apps.listReposAccessibleToInstallation,\n );\n // The return type of the paginate method is incorrect.\n const repositories: RestEndpointMethodTypes['apps']['listReposAccessibleToInstallation']['response']['data']['repositories'] =\n repos.repositories ?? repos;\n\n repositoryNames = repositories.map(repository => repository.name);\n }\n return {\n token: result.data.token,\n expiresAt: DateTime.fromISO(result.data.expires_at),\n repositories: repositoryNames,\n };\n });\n }\n\n async getPublicInstallationToken(): Promise<{ accessToken: string }> {\n const [installation] = await this.getCachedInstallations();\n\n if (!installation) {\n throw new Error(`No installation found for public app`);\n }\n\n return this.cache.getOrCreateToken(\n `public:${installation.id}`,\n undefined,\n async () => {\n const result = await this.createInstallationAccessToken(\n installation.id,\n );\n\n return {\n token: result.data.token,\n expiresAt: DateTime.fromISO(result.data.expires_at),\n };\n },\n );\n }\n\n private async createInstallationAccessToken(installationId: number) {\n try {\n return await this.appClient.apps.createInstallationAccessToken({\n installation_id: installationId,\n headers: HEADERS,\n });\n } catch (error) {\n // A 404/410 means the installation referenced in our cache no longer\n // exists, so drop the cache to force a refresh on the next lookup.\n if (isStaleInstallationError(error)) {\n this.installationsCache = undefined;\n }\n throw error;\n }\n }\n\n async getInstallations(): Promise<Installations> {\n return cloneDeep(await this.getCachedInstallations());\n }\n\n private async getCachedInstallations(\n options: { forceRefresh?: boolean } = {},\n ): Promise<Installations> {\n if (\n !options.forceRefresh &&\n this.installationsCache &&\n DateTime.local() < this.installationsCache.expiresAt\n ) {\n return this.installationsCache.data;\n }\n if (!this.pendingInstallations) {\n const pending = this.appClient\n .paginate(this.appClient.apps.listInstallations)\n .then(data => {\n const now = DateTime.local();\n this.installationsCache = {\n data,\n fetchedAt: now,\n expiresAt: now.plus({ minutes: INSTALLATIONS_CACHE_TTL_MINUTES }),\n };\n return data;\n })\n .finally(() => {\n this.lastInstallationsRefreshAttempt = DateTime.local();\n if (this.pendingInstallations === pending) {\n this.pendingInstallations = undefined;\n }\n });\n this.pendingInstallations = pending;\n }\n return await this.pendingInstallations;\n }\n\n private async getInstallationData(owner: string): Promise<InstallationData> {\n const ownerLower = owner.toLocaleLowerCase('en-US');\n const find = (list: Installations) =>\n list.find(\n inst =>\n inst.account &&\n 'login' in inst.account &&\n inst.account.login?.toLocaleLowerCase('en-US') === ownerLower,\n );\n\n let installations = await this.getCachedInstallations();\n let installation = find(installations);\n\n // Owner not in cache — a newly-created installation may have appeared\n // since we last paginated. Force a refresh (throttled) before failing.\n if (!installation && this.canRefreshInstallations()) {\n installations = await this.getCachedInstallations({\n forceRefresh: true,\n });\n installation = find(installations);\n }\n\n if (installation) {\n return {\n installationId: installation.id,\n suspended: Boolean(installation.suspended_by),\n };\n }\n\n const notFoundError = new Error(\n `No app installation found for ${owner} in ${this.baseAuthConfig.appId}`,\n );\n notFoundError.name = 'NotFoundError';\n throw notFoundError;\n }\n\n private canRefreshInstallations(): boolean {\n if (!this.installationsCache) {\n return true;\n }\n const refreshReference =\n this.lastInstallationsRefreshAttempt ?? this.installationsCache.fetchedAt;\n const age = DateTime.local().diff(refreshReference).as('seconds');\n return age >= INSTALLATIONS_REFRESH_THROTTLE_SECONDS;\n }\n}\n\n/**\n * Corresponds to a Github installation which internally could hold several GitHub Apps.\n *\n * @public\n */\nexport class GithubAppCredentialsMux {\n private readonly apps: GithubAppManager[];\n\n constructor(config: GithubIntegrationConfig, appIds: number[] = []) {\n this.apps =\n config.apps\n ?.filter(app => (appIds.length ? appIds.includes(app.appId) : true))\n .map(ac => new GithubAppManager(ac, config.apiBaseUrl)) ?? [];\n }\n\n async getAllInstallations(): Promise<\n RestEndpointMethodTypes['apps']['listInstallations']['response']['data']\n > {\n if (!this.apps.length) {\n return [];\n }\n\n const installs = await Promise.all(\n this.apps.map(app => app.getInstallations()),\n );\n\n return installs.flat();\n }\n\n async getAppToken(\n owner?: string,\n repo?: string,\n ): Promise<string | undefined> {\n if (this.apps.length === 0) {\n return undefined;\n }\n\n const results = await Promise.all(\n this.apps.map(app =>\n app.getInstallationCredentials(owner, repo).then(\n credentials => ({ credentials, error: undefined }),\n error => ({ credentials: undefined, error }),\n ),\n ),\n );\n\n const result = results.find(\n resultItem => resultItem.credentials?.accessToken,\n );\n\n if (result) {\n return result.credentials!.accessToken;\n }\n\n // If there was no token returned, then let's find a public access app and use an installation to get a token.\n const publicAccessApp = this.apps.find(app => app.publicAccess);\n if (publicAccessApp) {\n const publicResult = await publicAccessApp\n .getPublicInstallationToken()\n .then(\n credentials => ({ credentials, error: undefined }),\n error => ({ credentials: undefined, error }),\n );\n\n if (publicResult.credentials?.accessToken) {\n return publicResult.credentials.accessToken;\n }\n }\n\n const errors = results.map(r => r.error);\n const notNotFoundError = errors.find(err => err?.name !== 'NotFoundError');\n if (notNotFoundError) {\n throw notNotFoundError;\n }\n\n return undefined;\n }\n}\n\n/**\n * Handles the creation and caching of credentials for GitHub integrations.\n *\n * @public\n * @remarks\n *\n * TODO: Possibly move this to a backend only package so that it's not used in the frontend by mistake\n */\nexport class SingleInstanceGithubCredentialsProvider\n implements GithubCredentialsProvider\n{\n static create: (\n config: GithubIntegrationConfig,\n ) => GithubCredentialsProvider = config => {\n return new SingleInstanceGithubCredentialsProvider(\n new GithubAppCredentialsMux(config),\n config.token,\n );\n };\n\n private readonly githubAppCredentialsMux: GithubAppCredentialsMux;\n private readonly token?: string;\n\n private constructor(\n githubAppCredentialsMux: GithubAppCredentialsMux,\n token?: string,\n ) {\n this.githubAppCredentialsMux = githubAppCredentialsMux;\n this.token = token;\n }\n\n /**\n * Returns {@link GithubCredentials} for a given URL.\n *\n * @remarks\n *\n * Consecutive calls to this method with the same URL will return cached\n * credentials.\n *\n * The shortest lifetime for a token returned is 10 minutes.\n *\n * @example\n * ```ts\n * const { token, headers } = await getCredentials({\n * url: 'github.com/backstage/foobar'\n * })\n * ```\n *\n * @param opts - The organization or repository URL\n * @returns A promise of {@link GithubCredentials}.\n */\n async getCredentials(opts: { url: string }): Promise<GithubCredentials> {\n const parsed = parseGitUrl(opts.url);\n\n const owner = parsed.owner || parsed.name;\n const repo = parsed.owner ? parsed.name : undefined;\n\n let type: GithubCredentialType = 'app';\n let token = await this.githubAppCredentialsMux.getAppToken(owner, repo);\n if (!token) {\n type = 'token';\n token = this.token;\n }\n\n return {\n headers: token ? { Authorization: `Bearer ${token}` } : undefined,\n token,\n type,\n };\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/integration",
3
- "version": "2.1.0-next.0",
3
+ "version": "2.1.0",
4
4
  "description": "Helpers for managing integrations towards external systems",
5
5
  "backstage": {
6
6
  "role": "common-library"
@@ -61,9 +61,9 @@
61
61
  "dependencies": {
62
62
  "@azure/identity": "^4.0.0",
63
63
  "@azure/storage-blob": "^12.5.0",
64
- "@backstage/config": "1.3.8",
65
- "@backstage/connections": "0.3.0-next.1",
66
- "@backstage/errors": "1.3.1",
64
+ "@backstage/config": "^1.3.8",
65
+ "@backstage/connections": "^0.3.0",
66
+ "@backstage/errors": "^1.3.1",
67
67
  "@octokit/auth-app": "^4.0.0",
68
68
  "@octokit/rest": "^19.0.3",
69
69
  "cross-fetch": "^4.0.0",
@@ -73,9 +73,9 @@
73
73
  "p-throttle": "^4.1.1"
74
74
  },
75
75
  "devDependencies": {
76
- "@backstage/backend-test-utils": "1.11.6-next.0",
77
- "@backstage/cli": "0.36.5-next.0",
78
- "@backstage/config-loader": "1.11.0",
76
+ "@backstage/backend-test-utils": "^1.11.6",
77
+ "@backstage/cli": "^0.36.5",
78
+ "@backstage/config-loader": "^1.11.2",
79
79
  "msw": "^2.0.0"
80
80
  },
81
81
  "configSchema": "config.schema.json",