@backstage/integration 1.18.3-next.1 → 1.19.1

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,42 @@
1
1
  # @backstage/integration
2
2
 
3
+ ## 1.19.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 627c131: Rollback the lowercase replacing in GitHub integration config
8
+
9
+ ## 1.19.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 37fba1d: Added support for Bitbucket Cloud OAuth. This introduces an alternative authentication method using a workspace OAuth consumer, alongside App Passwords (deprecated) and API tokens. OAuth does not require a bot or service account and avoids token expiry issues.
14
+
15
+ **BREAKING CHANGES**
16
+
17
+ - **@backstage/integration** (`src/bitbucketCloud/core.ts`)
18
+
19
+ - `getBitbucketCloudRequestOptions` now returns a `Promise` and **must** be awaited.
20
+
21
+ - **@backstage/plugin-scaffolder-backend-module-bitbucket-cloud** (`src/actions/helpers.ts`)
22
+ - `getBitbucketClient` now returns a `Promise` and **must** be awaited.
23
+ - `getAuthorizationHeader` now returns a `Promise` and **must** be awaited.
24
+
25
+ **OAuth usage example**
26
+
27
+ ```yaml
28
+ integrations:
29
+ bitbucketCloud:
30
+ - clientId: client-id
31
+ clientSecret: client-secret
32
+ ```
33
+
34
+ ### Patch Changes
35
+
36
+ - a26a322: Added support for using a GitHub App installation to generate tokens for public repository access when the `publicAccess` option is enabled. When all other authentication methods fail (e.g., the app is not installed in that organization), the provider will now use an available installation to generate a token that can be used to access public repositories as read only.
37
+ - fb029b6: Updated luxon types
38
+ - e15fdae: Made the github urls case insensitive.
39
+
3
40
  ## 1.18.3-next.1
4
41
 
5
42
  ### Patch Changes
package/config.d.ts CHANGED
@@ -174,7 +174,7 @@ export interface Config {
174
174
  * The username to use for authenticated requests.
175
175
  * @visibility secret
176
176
  */
177
- username: string;
177
+ username?: string;
178
178
  /**
179
179
  * Token used to authenticate requests.
180
180
  * @visibility secret
@@ -186,6 +186,16 @@ export interface Config {
186
186
  * @deprecated Use `token` instead.
187
187
  */
188
188
  appPassword?: string;
189
+ /**
190
+ * OAuth client ID for Bitbucket Cloud.
191
+ * @visibility secret
192
+ */
193
+ clientId?: string;
194
+ /**
195
+ * OAuth client secret for Bitbucket Cloud.
196
+ * @visibility secret
197
+ */
198
+ clientSecret?: string;
189
199
  /**
190
200
  * PGP signing key for signing commits.
191
201
  * @visibility secret
@@ -330,6 +340,10 @@ export interface Config {
330
340
  * https://docs.github.com/en/rest/reference/apps#list-installations-for-the-authenticated-app--code-samples
331
341
  */
332
342
  allowedInstallationOwners?: string[];
343
+ /**
344
+ * If true, then an installation token will be issued for access when no other token is available.
345
+ */
346
+ publicAccess?: boolean;
333
347
  }>;
334
348
  }>;
335
349
 
@@ -5,20 +5,29 @@ const BITBUCKET_CLOUD_API_BASE_URL = "https://api.bitbucket.org/2.0";
5
5
  function readBitbucketCloudIntegrationConfig(config) {
6
6
  const host = BITBUCKET_CLOUD_HOST;
7
7
  const apiBaseUrl = BITBUCKET_CLOUD_API_BASE_URL;
8
- const username = config.getString("username");
8
+ const username = config.getOptionalString("username");
9
9
  const appPassword = config.getOptionalString("appPassword")?.trim();
10
10
  const token = config.getOptionalString("token");
11
- if (!token && !appPassword) {
11
+ const clientId = config.getOptionalString("clientId")?.trim();
12
+ const clientSecret = config.getOptionalString("clientSecret")?.trim();
13
+ if (username && !token && !appPassword) {
12
14
  throw new Error(
13
15
  `Bitbucket Cloud integration must be configured with as username and either a token or an appPassword.`
14
16
  );
15
17
  }
18
+ if (clientId && !clientSecret || clientSecret && !clientId) {
19
+ throw new Error(
20
+ `Bitbucket Cloud integration has incomplete OAuth configuration. Both clientId and clientSecret are required.`
21
+ );
22
+ }
16
23
  return {
17
24
  host,
18
25
  apiBaseUrl,
19
26
  username,
20
27
  appPassword,
21
28
  token,
29
+ clientId,
30
+ clientSecret,
22
31
  commitSigningKey: config.getOptionalString("commitSigningKey")
23
32
  };
24
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.cjs.js","sources":["../../src/bitbucketCloud/config.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { Config } from '@backstage/config';\n\nconst BITBUCKET_CLOUD_HOST = 'bitbucket.org';\nconst BITBUCKET_CLOUD_API_BASE_URL = 'https://api.bitbucket.org/2.0';\n\n/**\n * The configuration parameters for a single Bitbucket Cloud API provider.\n *\n * @public\n */\nexport type BitbucketCloudIntegrationConfig = {\n /**\n * Constant. bitbucket.org\n */\n host: string;\n\n /**\n * Constant. https://api.bitbucket.org/2.0\n */\n apiBaseUrl: string;\n\n /**\n * The username to use for requests to Bitbucket Cloud (bitbucket.org).\n */\n username?: string;\n\n /**\n * Authentication with Bitbucket Cloud (bitbucket.org) is done using app passwords.\n *\n * See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/\n */\n appPassword?: string;\n\n /**\n * The access token to use for requests to Bitbucket Cloud (bitbucket.org).\n *\n * See https://support.atlassian.com/bitbucket-cloud/docs/api-tokens/\n */\n token?: string;\n\n /** PGP private key for signing commits. */\n commitSigningKey?: string;\n};\n\n/**\n * Reads a single Bitbucket Cloud integration config.\n *\n * @param config - The config object of a single integration\n * @public\n */\nexport function readBitbucketCloudIntegrationConfig(\n config: Config,\n): BitbucketCloudIntegrationConfig {\n const host = BITBUCKET_CLOUD_HOST;\n const apiBaseUrl = BITBUCKET_CLOUD_API_BASE_URL;\n // If config is provided, we assume authenticated access is desired\n // (as the anonymous one is provided by default).\n const username = config.getString('username');\n // TODO: appPassword can be removed once fully\n // deprecated by BitBucket on 9th June 2026.\n const appPassword = config.getOptionalString('appPassword')?.trim();\n const token = config.getOptionalString('token');\n\n if (!token && !appPassword) {\n throw new Error(\n `Bitbucket Cloud integration must be configured with as username and either a token or an appPassword.`,\n );\n }\n\n return {\n host,\n apiBaseUrl,\n username,\n appPassword,\n token,\n commitSigningKey: config.getOptionalString('commitSigningKey'),\n };\n}\n\n/**\n * Reads a set of Bitbucket Cloud integration configs,\n * and inserts one for public Bitbucket Cloud if none specified.\n *\n * @param configs - All of the integration config objects\n * @public\n */\nexport function readBitbucketCloudIntegrationConfigs(\n configs: Config[],\n): BitbucketCloudIntegrationConfig[] {\n // First read all the explicit integrations\n const result = configs.map(readBitbucketCloudIntegrationConfig);\n\n // If no explicit bitbucket.org integration was added,\n // put one in the list as a convenience\n if (result.length === 0) {\n result.push({\n host: BITBUCKET_CLOUD_HOST,\n apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL,\n });\n }\n\n return result;\n}\n"],"names":[],"mappings":";;AAkBA,MAAM,oBAAA,GAAuB,eAAA;AAC7B,MAAM,4BAAA,GAA+B,+BAAA;AA+C9B,SAAS,oCACd,MAAA,EACiC;AACjC,EAAA,MAAM,IAAA,GAAO,oBAAA;AACb,EAAA,MAAM,UAAA,GAAa,4BAAA;AAGnB,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,SAAA,CAAU,UAAU,CAAA;AAG5C,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,iBAAA,CAAkB,aAAa,GAAG,IAAA,EAAK;AAClE,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,iBAAA,CAAkB,OAAO,CAAA;AAE9C,EAAA,IAAI,CAAC,KAAA,IAAS,CAAC,WAAA,EAAa;AAC1B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,qGAAA;AAAA,KACF;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,UAAA;AAAA,IACA,QAAA;AAAA,IACA,WAAA;AAAA,IACA,KAAA;AAAA,IACA,gBAAA,EAAkB,MAAA,CAAO,iBAAA,CAAkB,kBAAkB;AAAA,GAC/D;AACF;AASO,SAAS,qCACd,OAAA,EACmC;AAEnC,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,mCAAmC,CAAA;AAI9D,EAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACvB,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,IAAA,EAAM,oBAAA;AAAA,MACN,UAAA,EAAY;AAAA,KACb,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,MAAA;AACT;;;;;"}
1
+ {"version":3,"file":"config.cjs.js","sources":["../../src/bitbucketCloud/config.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { Config } from '@backstage/config';\n\nconst BITBUCKET_CLOUD_HOST = 'bitbucket.org';\nconst BITBUCKET_CLOUD_API_BASE_URL = 'https://api.bitbucket.org/2.0';\n\n/**\n * The configuration parameters for a single Bitbucket Cloud API provider.\n *\n * @public\n */\nexport type BitbucketCloudIntegrationConfig = {\n /**\n * Constant. bitbucket.org\n */\n host: string;\n\n /**\n * Constant. https://api.bitbucket.org/2.0\n */\n apiBaseUrl: string;\n\n /**\n * The username to use for requests to Bitbucket Cloud (bitbucket.org).\n */\n username?: string;\n\n /**\n * Authentication with Bitbucket Cloud (bitbucket.org) is done using app passwords.\n *\n * See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/\n */\n appPassword?: string;\n\n /**\n * The access token to use for requests to Bitbucket Cloud (bitbucket.org).\n *\n * See https://support.atlassian.com/bitbucket-cloud/docs/api-tokens/\n */\n token?: string;\n\n /**\n * The OAuth client ID for Bitbucket Cloud.\n *\n * See https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/\n */\n clientId?: string;\n\n /**\n * The OAuth client secret for Bitbucket Cloud.\n *\n * See https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/\n */\n clientSecret?: string;\n\n /** PGP private key for signing commits. */\n commitSigningKey?: string;\n};\n\n/**\n * Reads a single Bitbucket Cloud integration config.\n *\n * @param config - The config object of a single integration\n * @public\n */\nexport function readBitbucketCloudIntegrationConfig(\n config: Config,\n): BitbucketCloudIntegrationConfig {\n const host = BITBUCKET_CLOUD_HOST;\n const apiBaseUrl = BITBUCKET_CLOUD_API_BASE_URL;\n // If config is provided, we assume authenticated access is desired\n // (as the anonymous one is provided by default).\n const username = config.getOptionalString('username');\n // TODO: appPassword can be removed once fully\n // deprecated by BitBucket on 9th June 2026.\n const appPassword = config.getOptionalString('appPassword')?.trim();\n const token = config.getOptionalString('token');\n const clientId = config.getOptionalString('clientId')?.trim();\n const clientSecret = config.getOptionalString('clientSecret')?.trim();\n\n // Validate: if username is provided, token or appPassword is required\n if (username && !token && !appPassword) {\n throw new Error(\n `Bitbucket Cloud integration must be configured with as username and either a token or an appPassword.`,\n );\n }\n\n // Validate: OAuth requires both clientId and clientSecret\n if ((clientId && !clientSecret) || (clientSecret && !clientId)) {\n throw new Error(\n `Bitbucket Cloud integration has incomplete OAuth configuration. Both clientId and clientSecret are required.`,\n );\n }\n\n return {\n host,\n apiBaseUrl,\n username,\n appPassword,\n token,\n clientId,\n clientSecret,\n commitSigningKey: config.getOptionalString('commitSigningKey'),\n };\n}\n\n/**\n * Reads a set of Bitbucket Cloud integration configs,\n * and inserts one for public Bitbucket Cloud if none specified.\n *\n * @param configs - All of the integration config objects\n * @public\n */\nexport function readBitbucketCloudIntegrationConfigs(\n configs: Config[],\n): BitbucketCloudIntegrationConfig[] {\n // First read all the explicit integrations\n const result = configs.map(readBitbucketCloudIntegrationConfig);\n\n // If no explicit bitbucket.org integration was added,\n // put one in the list as a convenience\n if (result.length === 0) {\n result.push({\n host: BITBUCKET_CLOUD_HOST,\n apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL,\n });\n }\n\n return result;\n}\n"],"names":[],"mappings":";;AAkBA,MAAM,oBAAA,GAAuB,eAAA;AAC7B,MAAM,4BAAA,GAA+B,+BAAA;AA6D9B,SAAS,oCACd,MAAA,EACiC;AACjC,EAAA,MAAM,IAAA,GAAO,oBAAA;AACb,EAAA,MAAM,UAAA,GAAa,4BAAA;AAGnB,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,iBAAA,CAAkB,UAAU,CAAA;AAGpD,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,iBAAA,CAAkB,aAAa,GAAG,IAAA,EAAK;AAClE,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,iBAAA,CAAkB,OAAO,CAAA;AAC9C,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,iBAAA,CAAkB,UAAU,GAAG,IAAA,EAAK;AAC5D,EAAA,MAAM,YAAA,GAAe,MAAA,CAAO,iBAAA,CAAkB,cAAc,GAAG,IAAA,EAAK;AAGpE,EAAA,IAAI,QAAA,IAAY,CAAC,KAAA,IAAS,CAAC,WAAA,EAAa;AACtC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,qGAAA;AAAA,KACF;AAAA,EACF;AAGA,EAAA,IAAK,QAAA,IAAY,CAAC,YAAA,IAAkB,YAAA,IAAgB,CAAC,QAAA,EAAW;AAC9D,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,4GAAA;AAAA,KACF;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,UAAA;AAAA,IACA,QAAA;AAAA,IACA,WAAA;AAAA,IACA,KAAA;AAAA,IACA,QAAA;AAAA,IACA,YAAA;AAAA,IACA,gBAAA,EAAkB,MAAA,CAAO,iBAAA,CAAkB,kBAAkB;AAAA,GAC/D;AACF;AASO,SAAS,qCACd,OAAA,EACmC;AAEnC,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,mCAAmC,CAAA;AAI9D,EAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACvB,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,IAAA,EAAM,oBAAA;AAAA,MACN,UAAA,EAAY;AAAA,KACb,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,MAAA;AACT;;;;;"}
@@ -3,20 +3,29 @@ const BITBUCKET_CLOUD_API_BASE_URL = "https://api.bitbucket.org/2.0";
3
3
  function readBitbucketCloudIntegrationConfig(config) {
4
4
  const host = BITBUCKET_CLOUD_HOST;
5
5
  const apiBaseUrl = BITBUCKET_CLOUD_API_BASE_URL;
6
- const username = config.getString("username");
6
+ const username = config.getOptionalString("username");
7
7
  const appPassword = config.getOptionalString("appPassword")?.trim();
8
8
  const token = config.getOptionalString("token");
9
- if (!token && !appPassword) {
9
+ const clientId = config.getOptionalString("clientId")?.trim();
10
+ const clientSecret = config.getOptionalString("clientSecret")?.trim();
11
+ if (username && !token && !appPassword) {
10
12
  throw new Error(
11
13
  `Bitbucket Cloud integration must be configured with as username and either a token or an appPassword.`
12
14
  );
13
15
  }
16
+ if (clientId && !clientSecret || clientSecret && !clientId) {
17
+ throw new Error(
18
+ `Bitbucket Cloud integration has incomplete OAuth configuration. Both clientId and clientSecret are required.`
19
+ );
20
+ }
14
21
  return {
15
22
  host,
16
23
  apiBaseUrl,
17
24
  username,
18
25
  appPassword,
19
26
  token,
27
+ clientId,
28
+ clientSecret,
20
29
  commitSigningKey: config.getOptionalString("commitSigningKey")
21
30
  };
22
31
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.esm.js","sources":["../../src/bitbucketCloud/config.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { Config } from '@backstage/config';\n\nconst BITBUCKET_CLOUD_HOST = 'bitbucket.org';\nconst BITBUCKET_CLOUD_API_BASE_URL = 'https://api.bitbucket.org/2.0';\n\n/**\n * The configuration parameters for a single Bitbucket Cloud API provider.\n *\n * @public\n */\nexport type BitbucketCloudIntegrationConfig = {\n /**\n * Constant. bitbucket.org\n */\n host: string;\n\n /**\n * Constant. https://api.bitbucket.org/2.0\n */\n apiBaseUrl: string;\n\n /**\n * The username to use for requests to Bitbucket Cloud (bitbucket.org).\n */\n username?: string;\n\n /**\n * Authentication with Bitbucket Cloud (bitbucket.org) is done using app passwords.\n *\n * See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/\n */\n appPassword?: string;\n\n /**\n * The access token to use for requests to Bitbucket Cloud (bitbucket.org).\n *\n * See https://support.atlassian.com/bitbucket-cloud/docs/api-tokens/\n */\n token?: string;\n\n /** PGP private key for signing commits. */\n commitSigningKey?: string;\n};\n\n/**\n * Reads a single Bitbucket Cloud integration config.\n *\n * @param config - The config object of a single integration\n * @public\n */\nexport function readBitbucketCloudIntegrationConfig(\n config: Config,\n): BitbucketCloudIntegrationConfig {\n const host = BITBUCKET_CLOUD_HOST;\n const apiBaseUrl = BITBUCKET_CLOUD_API_BASE_URL;\n // If config is provided, we assume authenticated access is desired\n // (as the anonymous one is provided by default).\n const username = config.getString('username');\n // TODO: appPassword can be removed once fully\n // deprecated by BitBucket on 9th June 2026.\n const appPassword = config.getOptionalString('appPassword')?.trim();\n const token = config.getOptionalString('token');\n\n if (!token && !appPassword) {\n throw new Error(\n `Bitbucket Cloud integration must be configured with as username and either a token or an appPassword.`,\n );\n }\n\n return {\n host,\n apiBaseUrl,\n username,\n appPassword,\n token,\n commitSigningKey: config.getOptionalString('commitSigningKey'),\n };\n}\n\n/**\n * Reads a set of Bitbucket Cloud integration configs,\n * and inserts one for public Bitbucket Cloud if none specified.\n *\n * @param configs - All of the integration config objects\n * @public\n */\nexport function readBitbucketCloudIntegrationConfigs(\n configs: Config[],\n): BitbucketCloudIntegrationConfig[] {\n // First read all the explicit integrations\n const result = configs.map(readBitbucketCloudIntegrationConfig);\n\n // If no explicit bitbucket.org integration was added,\n // put one in the list as a convenience\n if (result.length === 0) {\n result.push({\n host: BITBUCKET_CLOUD_HOST,\n apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL,\n });\n }\n\n return result;\n}\n"],"names":[],"mappings":"AAkBA,MAAM,oBAAA,GAAuB,eAAA;AAC7B,MAAM,4BAAA,GAA+B,+BAAA;AA+C9B,SAAS,oCACd,MAAA,EACiC;AACjC,EAAA,MAAM,IAAA,GAAO,oBAAA;AACb,EAAA,MAAM,UAAA,GAAa,4BAAA;AAGnB,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,SAAA,CAAU,UAAU,CAAA;AAG5C,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,iBAAA,CAAkB,aAAa,GAAG,IAAA,EAAK;AAClE,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,iBAAA,CAAkB,OAAO,CAAA;AAE9C,EAAA,IAAI,CAAC,KAAA,IAAS,CAAC,WAAA,EAAa;AAC1B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,qGAAA;AAAA,KACF;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,UAAA;AAAA,IACA,QAAA;AAAA,IACA,WAAA;AAAA,IACA,KAAA;AAAA,IACA,gBAAA,EAAkB,MAAA,CAAO,iBAAA,CAAkB,kBAAkB;AAAA,GAC/D;AACF;AASO,SAAS,qCACd,OAAA,EACmC;AAEnC,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,mCAAmC,CAAA;AAI9D,EAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACvB,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,IAAA,EAAM,oBAAA;AAAA,MACN,UAAA,EAAY;AAAA,KACb,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,MAAA;AACT;;;;"}
1
+ {"version":3,"file":"config.esm.js","sources":["../../src/bitbucketCloud/config.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { Config } from '@backstage/config';\n\nconst BITBUCKET_CLOUD_HOST = 'bitbucket.org';\nconst BITBUCKET_CLOUD_API_BASE_URL = 'https://api.bitbucket.org/2.0';\n\n/**\n * The configuration parameters for a single Bitbucket Cloud API provider.\n *\n * @public\n */\nexport type BitbucketCloudIntegrationConfig = {\n /**\n * Constant. bitbucket.org\n */\n host: string;\n\n /**\n * Constant. https://api.bitbucket.org/2.0\n */\n apiBaseUrl: string;\n\n /**\n * The username to use for requests to Bitbucket Cloud (bitbucket.org).\n */\n username?: string;\n\n /**\n * Authentication with Bitbucket Cloud (bitbucket.org) is done using app passwords.\n *\n * See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/\n */\n appPassword?: string;\n\n /**\n * The access token to use for requests to Bitbucket Cloud (bitbucket.org).\n *\n * See https://support.atlassian.com/bitbucket-cloud/docs/api-tokens/\n */\n token?: string;\n\n /**\n * The OAuth client ID for Bitbucket Cloud.\n *\n * See https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/\n */\n clientId?: string;\n\n /**\n * The OAuth client secret for Bitbucket Cloud.\n *\n * See https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/\n */\n clientSecret?: string;\n\n /** PGP private key for signing commits. */\n commitSigningKey?: string;\n};\n\n/**\n * Reads a single Bitbucket Cloud integration config.\n *\n * @param config - The config object of a single integration\n * @public\n */\nexport function readBitbucketCloudIntegrationConfig(\n config: Config,\n): BitbucketCloudIntegrationConfig {\n const host = BITBUCKET_CLOUD_HOST;\n const apiBaseUrl = BITBUCKET_CLOUD_API_BASE_URL;\n // If config is provided, we assume authenticated access is desired\n // (as the anonymous one is provided by default).\n const username = config.getOptionalString('username');\n // TODO: appPassword can be removed once fully\n // deprecated by BitBucket on 9th June 2026.\n const appPassword = config.getOptionalString('appPassword')?.trim();\n const token = config.getOptionalString('token');\n const clientId = config.getOptionalString('clientId')?.trim();\n const clientSecret = config.getOptionalString('clientSecret')?.trim();\n\n // Validate: if username is provided, token or appPassword is required\n if (username && !token && !appPassword) {\n throw new Error(\n `Bitbucket Cloud integration must be configured with as username and either a token or an appPassword.`,\n );\n }\n\n // Validate: OAuth requires both clientId and clientSecret\n if ((clientId && !clientSecret) || (clientSecret && !clientId)) {\n throw new Error(\n `Bitbucket Cloud integration has incomplete OAuth configuration. Both clientId and clientSecret are required.`,\n );\n }\n\n return {\n host,\n apiBaseUrl,\n username,\n appPassword,\n token,\n clientId,\n clientSecret,\n commitSigningKey: config.getOptionalString('commitSigningKey'),\n };\n}\n\n/**\n * Reads a set of Bitbucket Cloud integration configs,\n * and inserts one for public Bitbucket Cloud if none specified.\n *\n * @param configs - All of the integration config objects\n * @public\n */\nexport function readBitbucketCloudIntegrationConfigs(\n configs: Config[],\n): BitbucketCloudIntegrationConfig[] {\n // First read all the explicit integrations\n const result = configs.map(readBitbucketCloudIntegrationConfig);\n\n // If no explicit bitbucket.org integration was added,\n // put one in the list as a convenience\n if (result.length === 0) {\n result.push({\n host: BITBUCKET_CLOUD_HOST,\n apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL,\n });\n }\n\n return result;\n}\n"],"names":[],"mappings":"AAkBA,MAAM,oBAAA,GAAuB,eAAA;AAC7B,MAAM,4BAAA,GAA+B,+BAAA;AA6D9B,SAAS,oCACd,MAAA,EACiC;AACjC,EAAA,MAAM,IAAA,GAAO,oBAAA;AACb,EAAA,MAAM,UAAA,GAAa,4BAAA;AAGnB,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,iBAAA,CAAkB,UAAU,CAAA;AAGpD,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,iBAAA,CAAkB,aAAa,GAAG,IAAA,EAAK;AAClE,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,iBAAA,CAAkB,OAAO,CAAA;AAC9C,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,iBAAA,CAAkB,UAAU,GAAG,IAAA,EAAK;AAC5D,EAAA,MAAM,YAAA,GAAe,MAAA,CAAO,iBAAA,CAAkB,cAAc,GAAG,IAAA,EAAK;AAGpE,EAAA,IAAI,QAAA,IAAY,CAAC,KAAA,IAAS,CAAC,WAAA,EAAa;AACtC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,qGAAA;AAAA,KACF;AAAA,EACF;AAGA,EAAA,IAAK,QAAA,IAAY,CAAC,YAAA,IAAkB,YAAA,IAAgB,CAAC,QAAA,EAAW;AAC9D,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,4GAAA;AAAA,KACF;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,UAAA;AAAA,IACA,QAAA;AAAA,IACA,WAAA;AAAA,IACA,KAAA;AAAA,IACA,QAAA;AAAA,IACA,YAAA;AAAA,IACA,gBAAA,EAAkB,MAAA,CAAO,iBAAA,CAAkB,kBAAkB;AAAA,GAC/D;AACF;AASO,SAAS,qCACd,OAAA,EACmC;AAEnC,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,mCAAmC,CAAA;AAI9D,EAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACvB,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,IAAA,EAAM,oBAAA;AAAA,MACN,UAAA,EAAY;AAAA,KACb,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,MAAA;AACT;;;;"}
@@ -2,18 +2,71 @@
2
2
 
3
3
  var fetch = require('cross-fetch');
4
4
  var parseGitUrl = require('git-url-parse');
5
+ var luxon = require('luxon');
5
6
 
6
7
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
7
8
 
8
9
  var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
9
10
  var parseGitUrl__default = /*#__PURE__*/_interopDefaultCompat(parseGitUrl);
10
11
 
12
+ let cachedToken;
13
+ let refreshPromise;
14
+ async function getBitbucketCloudOAuthToken(clientId, clientSecret) {
15
+ if (cachedToken && luxon.DateTime.now() < cachedToken.expiresAt) {
16
+ return cachedToken.token;
17
+ }
18
+ if (refreshPromise) {
19
+ return refreshPromise;
20
+ }
21
+ refreshPromise = (async () => {
22
+ try {
23
+ const credentials = Buffer.from(
24
+ `${clientId}:${clientSecret}`,
25
+ "utf8"
26
+ ).toString("base64");
27
+ const response = await fetch__default.default(
28
+ "https://bitbucket.org/site/oauth2/access_token",
29
+ {
30
+ method: "POST",
31
+ headers: {
32
+ "Content-Type": "application/x-www-form-urlencoded",
33
+ Authorization: `Basic ${credentials}`
34
+ },
35
+ body: "grant_type=client_credentials"
36
+ }
37
+ );
38
+ if (!response.ok) {
39
+ throw new Error(
40
+ `Failed to fetch OAuth token from Bitbucket Cloud: ${response.status} ${response.statusText}`
41
+ );
42
+ }
43
+ const data = await response.json();
44
+ if (!data.access_token) {
45
+ throw new Error("OAuth token response missing access_token field");
46
+ }
47
+ const expiresIn = data.expires_in || 3600;
48
+ const expiresAt = luxon.DateTime.now().plus({ seconds: expiresIn }).minus({ minutes: 10 });
49
+ cachedToken = {
50
+ token: data.access_token,
51
+ expiresAt
52
+ };
53
+ return data.access_token;
54
+ } catch (error) {
55
+ throw new Error(
56
+ `Failed to fetch OAuth token for Bitbucket Cloud: ${error}`
57
+ );
58
+ } finally {
59
+ refreshPromise = void 0;
60
+ }
61
+ })();
62
+ return refreshPromise;
63
+ }
11
64
  async function getBitbucketCloudDefaultBranch(url, config) {
12
65
  const { name: repoName, owner: project } = parseGitUrl__default.default(url);
13
66
  const branchUrl = `${config.apiBaseUrl}/repositories/${project}/${repoName}`;
14
67
  const response = await fetch__default.default(
15
68
  branchUrl,
16
- getBitbucketCloudRequestOptions(config)
69
+ await getBitbucketCloudRequestOptions(config)
17
70
  );
18
71
  if (!response.ok) {
19
72
  const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`;
@@ -57,8 +110,16 @@ function getBitbucketCloudFileFetchUrl(url, config) {
57
110
  throw new Error(`Incorrect URL: ${url}, ${e}`);
58
111
  }
59
112
  }
60
- function getBitbucketCloudRequestOptions(config) {
113
+ async function getBitbucketCloudRequestOptions(config) {
61
114
  const headers = {};
115
+ if (config.clientId && config.clientSecret) {
116
+ const token = await getBitbucketCloudOAuthToken(
117
+ config.clientId,
118
+ config.clientSecret
119
+ );
120
+ headers.Authorization = `Bearer ${token}`;
121
+ return { headers };
122
+ }
62
123
  if (config.username && (config.token ?? config.appPassword)) {
63
124
  const buffer = Buffer.from(
64
125
  `${config.username}:${config.token ?? config.appPassword}`,
@@ -72,5 +133,6 @@ function getBitbucketCloudRequestOptions(config) {
72
133
  exports.getBitbucketCloudDefaultBranch = getBitbucketCloudDefaultBranch;
73
134
  exports.getBitbucketCloudDownloadUrl = getBitbucketCloudDownloadUrl;
74
135
  exports.getBitbucketCloudFileFetchUrl = getBitbucketCloudFileFetchUrl;
136
+ exports.getBitbucketCloudOAuthToken = getBitbucketCloudOAuthToken;
75
137
  exports.getBitbucketCloudRequestOptions = getBitbucketCloudRequestOptions;
76
138
  //# sourceMappingURL=core.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"core.cjs.js","sources":["../../src/bitbucketCloud/core.ts"],"sourcesContent":["/*\n * Copyright 2020 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 fetch from 'cross-fetch';\nimport parseGitUrl from 'git-url-parse';\nimport { BitbucketCloudIntegrationConfig } from './config';\n\n/**\n * Given a URL pointing to a path on a provider, returns the default branch.\n *\n * @param url - A URL pointing to a path\n * @param config - The relevant provider config\n * @public\n */\nexport async function getBitbucketCloudDefaultBranch(\n url: string,\n config: BitbucketCloudIntegrationConfig,\n): Promise<string> {\n const { name: repoName, owner: project } = parseGitUrl(url);\n\n const branchUrl = `${config.apiBaseUrl}/repositories/${project}/${repoName}`;\n const response = await fetch(\n branchUrl,\n getBitbucketCloudRequestOptions(config),\n );\n\n if (!response.ok) {\n const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`;\n throw new Error(message);\n }\n\n const repoInfo = await response.json();\n const defaultBranch = repoInfo.mainbranch.name;\n if (!defaultBranch) {\n throw new Error(\n `Failed to read default branch from ${branchUrl}. ` +\n `Response ${response.status} ${response.json()}`,\n );\n }\n return defaultBranch;\n}\n\n/**\n * Given a URL pointing to a path on a provider, returns a URL that is suitable\n * for downloading the subtree.\n *\n * @param url - A URL pointing to a path\n * @param config - The relevant provider config\n * @public\n */\nexport async function getBitbucketCloudDownloadUrl(\n url: string,\n config: BitbucketCloudIntegrationConfig,\n): Promise<string> {\n const {\n name: repoName,\n owner: project,\n ref,\n protocol,\n resource,\n } = parseGitUrl(url);\n\n let branch = ref;\n if (!branch) {\n branch = await getBitbucketCloudDefaultBranch(url, config);\n }\n return `${protocol}://${resource}/${project}/${repoName}/get/${branch}.tar.gz`;\n}\n\n/**\n * Given a URL pointing to a file on a provider, returns a URL that is suitable\n * for fetching the contents of the data.\n *\n * @remarks\n *\n * Converts\n * from: https://bitbucket.org/orgname/reponame/src/master/file.yaml\n * to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml\n *\n * @param url - A URL pointing to a file\n * @param config - The relevant provider config\n * @public\n */\nexport function getBitbucketCloudFileFetchUrl(\n url: string,\n config: BitbucketCloudIntegrationConfig,\n): string {\n try {\n const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url);\n if (!owner || !name || (filepathtype !== 'src' && filepathtype !== 'raw')) {\n throw new Error('Invalid Bitbucket Cloud URL or file path');\n }\n\n const pathWithoutSlash = filepath.replace(/^\\//, '');\n\n if (!ref) {\n throw new Error('Invalid Bitbucket Cloud URL or file path');\n }\n return `${config.apiBaseUrl}/repositories/${owner}/${name}/src/${ref}/${pathWithoutSlash}`;\n } catch (e) {\n throw new Error(`Incorrect URL: ${url}, ${e}`);\n }\n}\n\n/**\n * Gets the request options necessary to make requests to a given provider.\n * Returns headers for authenticating with Bitbucket Cloud.\n * Supports both username/token and username/appPassword auth.\n *\n * @param config - The relevant provider config\n * @public\n */\nexport function getBitbucketCloudRequestOptions(\n config: BitbucketCloudIntegrationConfig,\n): {\n headers: Record<string, string>;\n} {\n const headers: Record<string, string> = {};\n\n // TODO: appPassword can be removed once fully\n // deprecated by BitBucket on 9th June 2026.\n if (config.username && (config.token ?? config.appPassword)) {\n const buffer = Buffer.from(\n `${config.username}:${config.token ?? config.appPassword}`,\n 'utf8',\n );\n headers.Authorization = `Basic ${buffer.toString('base64')}`;\n }\n\n return { headers };\n}\n"],"names":["parseGitUrl","fetch"],"mappings":"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"}
1
+ {"version":3,"file":"core.cjs.js","sources":["../../src/bitbucketCloud/core.ts"],"sourcesContent":["/*\n * Copyright 2020 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 fetch from 'cross-fetch';\nimport parseGitUrl from 'git-url-parse';\nimport { BitbucketCloudIntegrationConfig } from './config';\nimport { DateTime } from 'luxon';\n\ntype OAuthTokenData = {\n token: string;\n expiresAt: DateTime;\n};\n\n// In-memory token cache (single entry since there's only one Bitbucket Cloud integration)\nlet cachedToken: OAuthTokenData | undefined;\n\n// Track in-flight token refresh request to prevent concurrent fetches\nlet refreshPromise: Promise<string> | undefined;\n\n/**\n * Fetches an OAuth access token from Bitbucket Cloud using client credentials flow.\n * Tokens are cached with a 10-minute grace period to account for clock skew.\n * Implements concurrent refresh protection to prevent multiple simultaneous token requests.\n *\n * @param clientId - OAuth client ID\n * @param clientSecret - OAuth client secret\n * @public\n */\nexport async function getBitbucketCloudOAuthToken(\n clientId: string,\n clientSecret: string,\n): Promise<string> {\n // Check cache\n if (cachedToken && DateTime.now() < cachedToken.expiresAt) {\n return cachedToken.token;\n }\n\n // Check if there's already a refresh in progress\n if (refreshPromise) {\n return refreshPromise;\n }\n\n // Start a new token fetch and track it\n refreshPromise = (async () => {\n try {\n // Fetch new token\n const credentials = Buffer.from(\n `${clientId}:${clientSecret}`,\n 'utf8',\n ).toString('base64');\n\n const response = await fetch(\n 'https://bitbucket.org/site/oauth2/access_token',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Authorization: `Basic ${credentials}`,\n },\n body: 'grant_type=client_credentials',\n },\n );\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch OAuth token from Bitbucket Cloud: ${response.status} ${response.statusText}`,\n );\n }\n\n const data = await response.json();\n\n if (!data.access_token) {\n throw new Error('OAuth token response missing access_token field');\n }\n\n // Calculate expiration with 10-minute grace period\n const expiresIn = data.expires_in || 3600; // Default to 1 hour\n const expiresAt = DateTime.now()\n .plus({ seconds: expiresIn })\n .minus({ minutes: 10 });\n\n // Cache the token\n cachedToken = {\n token: data.access_token,\n expiresAt,\n };\n\n return data.access_token;\n } catch (error) {\n throw new Error(\n `Failed to fetch OAuth token for Bitbucket Cloud: ${error}`,\n );\n } finally {\n // Clean up the in-flight promise tracking\n refreshPromise = undefined;\n }\n })();\n\n return refreshPromise;\n}\n\n/**\n * Given a URL pointing to a path on a provider, returns the default branch.\n *\n * @param url - A URL pointing to a path\n * @param config - The relevant provider config\n * @public\n */\nexport async function getBitbucketCloudDefaultBranch(\n url: string,\n config: BitbucketCloudIntegrationConfig,\n): Promise<string> {\n const { name: repoName, owner: project } = parseGitUrl(url);\n\n const branchUrl = `${config.apiBaseUrl}/repositories/${project}/${repoName}`;\n const response = await fetch(\n branchUrl,\n await getBitbucketCloudRequestOptions(config),\n );\n\n if (!response.ok) {\n const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`;\n throw new Error(message);\n }\n\n const repoInfo = await response.json();\n const defaultBranch = repoInfo.mainbranch.name;\n if (!defaultBranch) {\n throw new Error(\n `Failed to read default branch from ${branchUrl}. ` +\n `Response ${response.status} ${response.json()}`,\n );\n }\n return defaultBranch;\n}\n\n/**\n * Given a URL pointing to a path on a provider, returns a URL that is suitable\n * for downloading the subtree.\n *\n * @param url - A URL pointing to a path\n * @param config - The relevant provider config\n * @public\n */\nexport async function getBitbucketCloudDownloadUrl(\n url: string,\n config: BitbucketCloudIntegrationConfig,\n): Promise<string> {\n const {\n name: repoName,\n owner: project,\n ref,\n protocol,\n resource,\n } = parseGitUrl(url);\n\n let branch = ref;\n if (!branch) {\n branch = await getBitbucketCloudDefaultBranch(url, config);\n }\n return `${protocol}://${resource}/${project}/${repoName}/get/${branch}.tar.gz`;\n}\n\n/**\n * Given a URL pointing to a file on a provider, returns a URL that is suitable\n * for fetching the contents of the data.\n *\n * @remarks\n *\n * Converts\n * from: https://bitbucket.org/orgname/reponame/src/master/file.yaml\n * to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml\n *\n * @param url - A URL pointing to a file\n * @param config - The relevant provider config\n * @public\n */\nexport function getBitbucketCloudFileFetchUrl(\n url: string,\n config: BitbucketCloudIntegrationConfig,\n): string {\n try {\n const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url);\n if (!owner || !name || (filepathtype !== 'src' && filepathtype !== 'raw')) {\n throw new Error('Invalid Bitbucket Cloud URL or file path');\n }\n\n const pathWithoutSlash = filepath.replace(/^\\//, '');\n\n if (!ref) {\n throw new Error('Invalid Bitbucket Cloud URL or file path');\n }\n return `${config.apiBaseUrl}/repositories/${owner}/${name}/src/${ref}/${pathWithoutSlash}`;\n } catch (e) {\n throw new Error(`Incorrect URL: ${url}, ${e}`);\n }\n}\n\n/**\n * Gets the request options necessary to make requests to a given provider.\n * Returns headers for authenticating with Bitbucket Cloud.\n * Supports OAuth (clientId/clientSecret), username/token, and username/appPassword auth.\n *\n * @param config - The relevant provider config\n * @public\n */\nexport async function getBitbucketCloudRequestOptions(\n config: BitbucketCloudIntegrationConfig,\n): Promise<{\n headers: Record<string, string>;\n}> {\n const headers: Record<string, string> = {};\n\n // OAuth authentication (clientId/clientSecret)\n if (config.clientId && config.clientSecret) {\n const token = await getBitbucketCloudOAuthToken(\n config.clientId,\n config.clientSecret,\n );\n headers.Authorization = `Bearer ${token}`;\n return { headers };\n }\n\n // Basic authentication (username + token/appPassword)\n // TODO: appPassword can be removed once fully\n // deprecated by BitBucket on 9th June 2026.\n if (config.username && (config.token ?? config.appPassword)) {\n const buffer = Buffer.from(\n `${config.username}:${config.token ?? config.appPassword}`,\n 'utf8',\n );\n headers.Authorization = `Basic ${buffer.toString('base64')}`;\n }\n\n return { headers };\n}\n"],"names":["DateTime","fetch","parseGitUrl"],"mappings":"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"}
@@ -1,12 +1,65 @@
1
1
  import fetch from 'cross-fetch';
2
2
  import parseGitUrl from 'git-url-parse';
3
+ import { DateTime } from 'luxon';
3
4
 
5
+ let cachedToken;
6
+ let refreshPromise;
7
+ async function getBitbucketCloudOAuthToken(clientId, clientSecret) {
8
+ if (cachedToken && DateTime.now() < cachedToken.expiresAt) {
9
+ return cachedToken.token;
10
+ }
11
+ if (refreshPromise) {
12
+ return refreshPromise;
13
+ }
14
+ refreshPromise = (async () => {
15
+ try {
16
+ const credentials = Buffer.from(
17
+ `${clientId}:${clientSecret}`,
18
+ "utf8"
19
+ ).toString("base64");
20
+ const response = await fetch(
21
+ "https://bitbucket.org/site/oauth2/access_token",
22
+ {
23
+ method: "POST",
24
+ headers: {
25
+ "Content-Type": "application/x-www-form-urlencoded",
26
+ Authorization: `Basic ${credentials}`
27
+ },
28
+ body: "grant_type=client_credentials"
29
+ }
30
+ );
31
+ if (!response.ok) {
32
+ throw new Error(
33
+ `Failed to fetch OAuth token from Bitbucket Cloud: ${response.status} ${response.statusText}`
34
+ );
35
+ }
36
+ const data = await response.json();
37
+ if (!data.access_token) {
38
+ throw new Error("OAuth token response missing access_token field");
39
+ }
40
+ const expiresIn = data.expires_in || 3600;
41
+ const expiresAt = DateTime.now().plus({ seconds: expiresIn }).minus({ minutes: 10 });
42
+ cachedToken = {
43
+ token: data.access_token,
44
+ expiresAt
45
+ };
46
+ return data.access_token;
47
+ } catch (error) {
48
+ throw new Error(
49
+ `Failed to fetch OAuth token for Bitbucket Cloud: ${error}`
50
+ );
51
+ } finally {
52
+ refreshPromise = void 0;
53
+ }
54
+ })();
55
+ return refreshPromise;
56
+ }
4
57
  async function getBitbucketCloudDefaultBranch(url, config) {
5
58
  const { name: repoName, owner: project } = parseGitUrl(url);
6
59
  const branchUrl = `${config.apiBaseUrl}/repositories/${project}/${repoName}`;
7
60
  const response = await fetch(
8
61
  branchUrl,
9
- getBitbucketCloudRequestOptions(config)
62
+ await getBitbucketCloudRequestOptions(config)
10
63
  );
11
64
  if (!response.ok) {
12
65
  const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`;
@@ -50,8 +103,16 @@ function getBitbucketCloudFileFetchUrl(url, config) {
50
103
  throw new Error(`Incorrect URL: ${url}, ${e}`);
51
104
  }
52
105
  }
53
- function getBitbucketCloudRequestOptions(config) {
106
+ async function getBitbucketCloudRequestOptions(config) {
54
107
  const headers = {};
108
+ if (config.clientId && config.clientSecret) {
109
+ const token = await getBitbucketCloudOAuthToken(
110
+ config.clientId,
111
+ config.clientSecret
112
+ );
113
+ headers.Authorization = `Bearer ${token}`;
114
+ return { headers };
115
+ }
55
116
  if (config.username && (config.token ?? config.appPassword)) {
56
117
  const buffer = Buffer.from(
57
118
  `${config.username}:${config.token ?? config.appPassword}`,
@@ -62,5 +123,5 @@ function getBitbucketCloudRequestOptions(config) {
62
123
  return { headers };
63
124
  }
64
125
 
65
- export { getBitbucketCloudDefaultBranch, getBitbucketCloudDownloadUrl, getBitbucketCloudFileFetchUrl, getBitbucketCloudRequestOptions };
126
+ export { getBitbucketCloudDefaultBranch, getBitbucketCloudDownloadUrl, getBitbucketCloudFileFetchUrl, getBitbucketCloudOAuthToken, getBitbucketCloudRequestOptions };
66
127
  //# sourceMappingURL=core.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"core.esm.js","sources":["../../src/bitbucketCloud/core.ts"],"sourcesContent":["/*\n * Copyright 2020 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 fetch from 'cross-fetch';\nimport parseGitUrl from 'git-url-parse';\nimport { BitbucketCloudIntegrationConfig } from './config';\n\n/**\n * Given a URL pointing to a path on a provider, returns the default branch.\n *\n * @param url - A URL pointing to a path\n * @param config - The relevant provider config\n * @public\n */\nexport async function getBitbucketCloudDefaultBranch(\n url: string,\n config: BitbucketCloudIntegrationConfig,\n): Promise<string> {\n const { name: repoName, owner: project } = parseGitUrl(url);\n\n const branchUrl = `${config.apiBaseUrl}/repositories/${project}/${repoName}`;\n const response = await fetch(\n branchUrl,\n getBitbucketCloudRequestOptions(config),\n );\n\n if (!response.ok) {\n const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`;\n throw new Error(message);\n }\n\n const repoInfo = await response.json();\n const defaultBranch = repoInfo.mainbranch.name;\n if (!defaultBranch) {\n throw new Error(\n `Failed to read default branch from ${branchUrl}. ` +\n `Response ${response.status} ${response.json()}`,\n );\n }\n return defaultBranch;\n}\n\n/**\n * Given a URL pointing to a path on a provider, returns a URL that is suitable\n * for downloading the subtree.\n *\n * @param url - A URL pointing to a path\n * @param config - The relevant provider config\n * @public\n */\nexport async function getBitbucketCloudDownloadUrl(\n url: string,\n config: BitbucketCloudIntegrationConfig,\n): Promise<string> {\n const {\n name: repoName,\n owner: project,\n ref,\n protocol,\n resource,\n } = parseGitUrl(url);\n\n let branch = ref;\n if (!branch) {\n branch = await getBitbucketCloudDefaultBranch(url, config);\n }\n return `${protocol}://${resource}/${project}/${repoName}/get/${branch}.tar.gz`;\n}\n\n/**\n * Given a URL pointing to a file on a provider, returns a URL that is suitable\n * for fetching the contents of the data.\n *\n * @remarks\n *\n * Converts\n * from: https://bitbucket.org/orgname/reponame/src/master/file.yaml\n * to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml\n *\n * @param url - A URL pointing to a file\n * @param config - The relevant provider config\n * @public\n */\nexport function getBitbucketCloudFileFetchUrl(\n url: string,\n config: BitbucketCloudIntegrationConfig,\n): string {\n try {\n const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url);\n if (!owner || !name || (filepathtype !== 'src' && filepathtype !== 'raw')) {\n throw new Error('Invalid Bitbucket Cloud URL or file path');\n }\n\n const pathWithoutSlash = filepath.replace(/^\\//, '');\n\n if (!ref) {\n throw new Error('Invalid Bitbucket Cloud URL or file path');\n }\n return `${config.apiBaseUrl}/repositories/${owner}/${name}/src/${ref}/${pathWithoutSlash}`;\n } catch (e) {\n throw new Error(`Incorrect URL: ${url}, ${e}`);\n }\n}\n\n/**\n * Gets the request options necessary to make requests to a given provider.\n * Returns headers for authenticating with Bitbucket Cloud.\n * Supports both username/token and username/appPassword auth.\n *\n * @param config - The relevant provider config\n * @public\n */\nexport function getBitbucketCloudRequestOptions(\n config: BitbucketCloudIntegrationConfig,\n): {\n headers: Record<string, string>;\n} {\n const headers: Record<string, string> = {};\n\n // TODO: appPassword can be removed once fully\n // deprecated by BitBucket on 9th June 2026.\n if (config.username && (config.token ?? config.appPassword)) {\n const buffer = Buffer.from(\n `${config.username}:${config.token ?? config.appPassword}`,\n 'utf8',\n );\n headers.Authorization = `Basic ${buffer.toString('base64')}`;\n }\n\n return { headers };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"core.esm.js","sources":["../../src/bitbucketCloud/core.ts"],"sourcesContent":["/*\n * Copyright 2020 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 fetch from 'cross-fetch';\nimport parseGitUrl from 'git-url-parse';\nimport { BitbucketCloudIntegrationConfig } from './config';\nimport { DateTime } from 'luxon';\n\ntype OAuthTokenData = {\n token: string;\n expiresAt: DateTime;\n};\n\n// In-memory token cache (single entry since there's only one Bitbucket Cloud integration)\nlet cachedToken: OAuthTokenData | undefined;\n\n// Track in-flight token refresh request to prevent concurrent fetches\nlet refreshPromise: Promise<string> | undefined;\n\n/**\n * Fetches an OAuth access token from Bitbucket Cloud using client credentials flow.\n * Tokens are cached with a 10-minute grace period to account for clock skew.\n * Implements concurrent refresh protection to prevent multiple simultaneous token requests.\n *\n * @param clientId - OAuth client ID\n * @param clientSecret - OAuth client secret\n * @public\n */\nexport async function getBitbucketCloudOAuthToken(\n clientId: string,\n clientSecret: string,\n): Promise<string> {\n // Check cache\n if (cachedToken && DateTime.now() < cachedToken.expiresAt) {\n return cachedToken.token;\n }\n\n // Check if there's already a refresh in progress\n if (refreshPromise) {\n return refreshPromise;\n }\n\n // Start a new token fetch and track it\n refreshPromise = (async () => {\n try {\n // Fetch new token\n const credentials = Buffer.from(\n `${clientId}:${clientSecret}`,\n 'utf8',\n ).toString('base64');\n\n const response = await fetch(\n 'https://bitbucket.org/site/oauth2/access_token',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Authorization: `Basic ${credentials}`,\n },\n body: 'grant_type=client_credentials',\n },\n );\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch OAuth token from Bitbucket Cloud: ${response.status} ${response.statusText}`,\n );\n }\n\n const data = await response.json();\n\n if (!data.access_token) {\n throw new Error('OAuth token response missing access_token field');\n }\n\n // Calculate expiration with 10-minute grace period\n const expiresIn = data.expires_in || 3600; // Default to 1 hour\n const expiresAt = DateTime.now()\n .plus({ seconds: expiresIn })\n .minus({ minutes: 10 });\n\n // Cache the token\n cachedToken = {\n token: data.access_token,\n expiresAt,\n };\n\n return data.access_token;\n } catch (error) {\n throw new Error(\n `Failed to fetch OAuth token for Bitbucket Cloud: ${error}`,\n );\n } finally {\n // Clean up the in-flight promise tracking\n refreshPromise = undefined;\n }\n })();\n\n return refreshPromise;\n}\n\n/**\n * Given a URL pointing to a path on a provider, returns the default branch.\n *\n * @param url - A URL pointing to a path\n * @param config - The relevant provider config\n * @public\n */\nexport async function getBitbucketCloudDefaultBranch(\n url: string,\n config: BitbucketCloudIntegrationConfig,\n): Promise<string> {\n const { name: repoName, owner: project } = parseGitUrl(url);\n\n const branchUrl = `${config.apiBaseUrl}/repositories/${project}/${repoName}`;\n const response = await fetch(\n branchUrl,\n await getBitbucketCloudRequestOptions(config),\n );\n\n if (!response.ok) {\n const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`;\n throw new Error(message);\n }\n\n const repoInfo = await response.json();\n const defaultBranch = repoInfo.mainbranch.name;\n if (!defaultBranch) {\n throw new Error(\n `Failed to read default branch from ${branchUrl}. ` +\n `Response ${response.status} ${response.json()}`,\n );\n }\n return defaultBranch;\n}\n\n/**\n * Given a URL pointing to a path on a provider, returns a URL that is suitable\n * for downloading the subtree.\n *\n * @param url - A URL pointing to a path\n * @param config - The relevant provider config\n * @public\n */\nexport async function getBitbucketCloudDownloadUrl(\n url: string,\n config: BitbucketCloudIntegrationConfig,\n): Promise<string> {\n const {\n name: repoName,\n owner: project,\n ref,\n protocol,\n resource,\n } = parseGitUrl(url);\n\n let branch = ref;\n if (!branch) {\n branch = await getBitbucketCloudDefaultBranch(url, config);\n }\n return `${protocol}://${resource}/${project}/${repoName}/get/${branch}.tar.gz`;\n}\n\n/**\n * Given a URL pointing to a file on a provider, returns a URL that is suitable\n * for fetching the contents of the data.\n *\n * @remarks\n *\n * Converts\n * from: https://bitbucket.org/orgname/reponame/src/master/file.yaml\n * to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml\n *\n * @param url - A URL pointing to a file\n * @param config - The relevant provider config\n * @public\n */\nexport function getBitbucketCloudFileFetchUrl(\n url: string,\n config: BitbucketCloudIntegrationConfig,\n): string {\n try {\n const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url);\n if (!owner || !name || (filepathtype !== 'src' && filepathtype !== 'raw')) {\n throw new Error('Invalid Bitbucket Cloud URL or file path');\n }\n\n const pathWithoutSlash = filepath.replace(/^\\//, '');\n\n if (!ref) {\n throw new Error('Invalid Bitbucket Cloud URL or file path');\n }\n return `${config.apiBaseUrl}/repositories/${owner}/${name}/src/${ref}/${pathWithoutSlash}`;\n } catch (e) {\n throw new Error(`Incorrect URL: ${url}, ${e}`);\n }\n}\n\n/**\n * Gets the request options necessary to make requests to a given provider.\n * Returns headers for authenticating with Bitbucket Cloud.\n * Supports OAuth (clientId/clientSecret), username/token, and username/appPassword auth.\n *\n * @param config - The relevant provider config\n * @public\n */\nexport async function getBitbucketCloudRequestOptions(\n config: BitbucketCloudIntegrationConfig,\n): Promise<{\n headers: Record<string, string>;\n}> {\n const headers: Record<string, string> = {};\n\n // OAuth authentication (clientId/clientSecret)\n if (config.clientId && config.clientSecret) {\n const token = await getBitbucketCloudOAuthToken(\n config.clientId,\n config.clientSecret,\n );\n headers.Authorization = `Bearer ${token}`;\n return { headers };\n }\n\n // Basic authentication (username + token/appPassword)\n // TODO: appPassword can be removed once fully\n // deprecated by BitBucket on 9th June 2026.\n if (config.username && (config.token ?? config.appPassword)) {\n const buffer = Buffer.from(\n `${config.username}:${config.token ?? config.appPassword}`,\n 'utf8',\n );\n headers.Authorization = `Basic ${buffer.toString('base64')}`;\n }\n\n return { headers };\n}\n"],"names":[],"mappings":"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"}
@@ -41,9 +41,7 @@ function replaceGithubUrlType(url, type) {
41
41
  return url.replace(
42
42
  /\/\/([^/]+)\/([^/]+)\/([^/]+)\/(blob|tree|edit)\//,
43
43
  (_, host, owner, repo) => {
44
- return `//${host.toLocaleLowerCase("en-US")}/${owner.toLocaleLowerCase(
45
- "en-US"
46
- )}/${repo.toLocaleLowerCase("en-US")}/${type}/`;
44
+ return `//${host}/${owner}/${repo}/${type}/`;
47
45
  }
48
46
  );
49
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"GithubIntegration.cjs.js","sources":["../../src/github/GithubIntegration.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { basicIntegrations, defaultScmResolveUrl } from '../helpers';\nimport {\n RateLimitInfo,\n ScmIntegration,\n ScmIntegrationsFactory,\n} from '../types';\nimport {\n GithubIntegrationConfig,\n readGithubIntegrationConfigs,\n} from './config';\nimport { ConsumedResponse } from '@backstage/errors';\n\n/**\n * A GitHub based integration.\n *\n * @public\n */\nexport class GithubIntegration implements ScmIntegration {\n static factory: ScmIntegrationsFactory<GithubIntegration> = ({ config }) => {\n const configs = readGithubIntegrationConfigs(\n config.getOptionalConfigArray('integrations.github') ?? [],\n );\n return basicIntegrations(\n configs.map(c => new GithubIntegration(c)),\n i => i.config.host,\n );\n };\n\n constructor(private readonly integrationConfig: GithubIntegrationConfig) {}\n\n get type(): string {\n return 'github';\n }\n\n get title(): string {\n return this.integrationConfig.host;\n }\n\n get config(): GithubIntegrationConfig {\n return this.integrationConfig;\n }\n\n resolveUrl(options: {\n url: string;\n base: string;\n lineNumber?: number;\n }): string {\n // GitHub uses blob URLs for files and tree urls for directory listings. But\n // there is a redirect from tree to blob for files, so we can always return\n // tree urls here.\n return replaceGithubUrlType(defaultScmResolveUrl(options), 'tree');\n }\n\n resolveEditUrl(url: string): string {\n return replaceGithubUrlType(url, 'edit');\n }\n\n parseRateLimitInfo(response: ConsumedResponse): RateLimitInfo {\n return {\n isRateLimited:\n response.status === 429 ||\n (response.status === 403 &&\n response.headers.get('x-ratelimit-remaining') === '0'),\n };\n }\n}\n\n/**\n * Takes a GitHub URL and replaces the type part (blob, tree etc).\n *\n * @param url - The original URL\n * @param type - The desired type, e.g. \"blob\"\n * @public\n */\nexport function replaceGithubUrlType(\n url: string,\n type: 'blob' | 'tree' | 'edit',\n): string {\n return url.replace(\n /\\/\\/([^/]+)\\/([^/]+)\\/([^/]+)\\/(blob|tree|edit)\\//,\n (_, host, owner, repo) => {\n return `//${host.toLocaleLowerCase('en-US')}/${owner.toLocaleLowerCase(\n 'en-US',\n )}/${repo.toLocaleLowerCase('en-US')}/${type}/`;\n },\n );\n}\n"],"names":["config","readGithubIntegrationConfigs","basicIntegrations","defaultScmResolveUrl"],"mappings":";;;;;AAiCO,MAAM,iBAAA,CAA4C;AAAA,EAWvD,YAA6B,iBAAA,EAA4C;AAA5C,IAAA,IAAA,CAAA,iBAAA,GAAA,iBAAA;AAAA,EAA6C;AAAA,EAV1E,OAAO,OAAA,GAAqD,CAAC,UAAEA,UAAO,KAAM;AAC1E,IAAA,MAAM,OAAA,GAAUC,mCAAA;AAAA,MACdD,QAAA,CAAO,sBAAA,CAAuB,qBAAqB,CAAA,IAAK;AAAC,KAC3D;AACA,IAAA,OAAOE,yBAAA;AAAA,MACL,QAAQ,GAAA,CAAI,CAAA,CAAA,KAAK,IAAI,iBAAA,CAAkB,CAAC,CAAC,CAAA;AAAA,MACzC,CAAA,CAAA,KAAK,EAAE,MAAA,CAAO;AAAA,KAChB;AAAA,EACF,CAAA;AAAA,EAIA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,QAAA;AAAA,EACT;AAAA,EAEA,IAAI,KAAA,GAAgB;AAClB,IAAA,OAAO,KAAK,iBAAA,CAAkB,IAAA;AAAA,EAChC;AAAA,EAEA,IAAI,MAAA,GAAkC;AACpC,IAAA,OAAO,IAAA,CAAK,iBAAA;AAAA,EACd;AAAA,EAEA,WAAW,OAAA,EAIA;AAIT,IAAA,OAAO,oBAAA,CAAqBC,4BAAA,CAAqB,OAAO,CAAA,EAAG,MAAM,CAAA;AAAA,EACnE;AAAA,EAEA,eAAe,GAAA,EAAqB;AAClC,IAAA,OAAO,oBAAA,CAAqB,KAAK,MAAM,CAAA;AAAA,EACzC;AAAA,EAEA,mBAAmB,QAAA,EAA2C;AAC5D,IAAA,OAAO;AAAA,MACL,aAAA,EACE,QAAA,CAAS,MAAA,KAAW,GAAA,IACnB,QAAA,CAAS,MAAA,KAAW,GAAA,IACnB,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,uBAAuB,CAAA,KAAM;AAAA,KACxD;AAAA,EACF;AACF;AASO,SAAS,oBAAA,CACd,KACA,IAAA,EACQ;AACR,EAAA,OAAO,GAAA,CAAI,OAAA;AAAA,IACT,mDAAA;AAAA,IACA,CAAC,CAAA,EAAG,IAAA,EAAM,KAAA,EAAO,IAAA,KAAS;AACxB,MAAA,OAAO,KAAK,IAAA,CAAK,iBAAA,CAAkB,OAAO,CAAC,IAAI,KAAA,CAAM,iBAAA;AAAA,QACnD;AAAA,OACD,CAAA,CAAA,EAAI,IAAA,CAAK,kBAAkB,OAAO,CAAC,IAAI,IAAI,CAAA,CAAA,CAAA;AAAA,IAC9C;AAAA,GACF;AACF;;;;;"}
1
+ {"version":3,"file":"GithubIntegration.cjs.js","sources":["../../src/github/GithubIntegration.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { basicIntegrations, defaultScmResolveUrl } from '../helpers';\nimport {\n RateLimitInfo,\n ScmIntegration,\n ScmIntegrationsFactory,\n} from '../types';\nimport {\n GithubIntegrationConfig,\n readGithubIntegrationConfigs,\n} from './config';\nimport { ConsumedResponse } from '@backstage/errors';\n\n/**\n * A GitHub based integration.\n *\n * @public\n */\nexport class GithubIntegration implements ScmIntegration {\n static factory: ScmIntegrationsFactory<GithubIntegration> = ({ config }) => {\n const configs = readGithubIntegrationConfigs(\n config.getOptionalConfigArray('integrations.github') ?? [],\n );\n return basicIntegrations(\n configs.map(c => new GithubIntegration(c)),\n i => i.config.host,\n );\n };\n\n constructor(private readonly integrationConfig: GithubIntegrationConfig) {}\n\n get type(): string {\n return 'github';\n }\n\n get title(): string {\n return this.integrationConfig.host;\n }\n\n get config(): GithubIntegrationConfig {\n return this.integrationConfig;\n }\n\n resolveUrl(options: {\n url: string;\n base: string;\n lineNumber?: number;\n }): string {\n // GitHub uses blob URLs for files and tree urls for directory listings. But\n // there is a redirect from tree to blob for files, so we can always return\n // tree urls here.\n return replaceGithubUrlType(defaultScmResolveUrl(options), 'tree');\n }\n\n resolveEditUrl(url: string): string {\n return replaceGithubUrlType(url, 'edit');\n }\n\n parseRateLimitInfo(response: ConsumedResponse): RateLimitInfo {\n return {\n isRateLimited:\n response.status === 429 ||\n (response.status === 403 &&\n response.headers.get('x-ratelimit-remaining') === '0'),\n };\n }\n}\n\n/**\n * Takes a GitHub URL and replaces the type part (blob, tree etc).\n *\n * @param url - The original URL\n * @param type - The desired type, e.g. \"blob\"\n * @public\n */\nexport function replaceGithubUrlType(\n url: string,\n type: 'blob' | 'tree' | 'edit',\n): string {\n return url.replace(\n /\\/\\/([^/]+)\\/([^/]+)\\/([^/]+)\\/(blob|tree|edit)\\//,\n (_, host, owner, repo) => {\n return `//${host}/${owner}/${repo}/${type}/`;\n },\n );\n}\n"],"names":["config","readGithubIntegrationConfigs","basicIntegrations","defaultScmResolveUrl"],"mappings":";;;;;AAiCO,MAAM,iBAAA,CAA4C;AAAA,EAWvD,YAA6B,iBAAA,EAA4C;AAA5C,IAAA,IAAA,CAAA,iBAAA,GAAA,iBAAA;AAAA,EAA6C;AAAA,EAV1E,OAAO,OAAA,GAAqD,CAAC,UAAEA,UAAO,KAAM;AAC1E,IAAA,MAAM,OAAA,GAAUC,mCAAA;AAAA,MACdD,QAAA,CAAO,sBAAA,CAAuB,qBAAqB,CAAA,IAAK;AAAC,KAC3D;AACA,IAAA,OAAOE,yBAAA;AAAA,MACL,QAAQ,GAAA,CAAI,CAAA,CAAA,KAAK,IAAI,iBAAA,CAAkB,CAAC,CAAC,CAAA;AAAA,MACzC,CAAA,CAAA,KAAK,EAAE,MAAA,CAAO;AAAA,KAChB;AAAA,EACF,CAAA;AAAA,EAIA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,QAAA;AAAA,EACT;AAAA,EAEA,IAAI,KAAA,GAAgB;AAClB,IAAA,OAAO,KAAK,iBAAA,CAAkB,IAAA;AAAA,EAChC;AAAA,EAEA,IAAI,MAAA,GAAkC;AACpC,IAAA,OAAO,IAAA,CAAK,iBAAA;AAAA,EACd;AAAA,EAEA,WAAW,OAAA,EAIA;AAIT,IAAA,OAAO,oBAAA,CAAqBC,4BAAA,CAAqB,OAAO,CAAA,EAAG,MAAM,CAAA;AAAA,EACnE;AAAA,EAEA,eAAe,GAAA,EAAqB;AAClC,IAAA,OAAO,oBAAA,CAAqB,KAAK,MAAM,CAAA;AAAA,EACzC;AAAA,EAEA,mBAAmB,QAAA,EAA2C;AAC5D,IAAA,OAAO;AAAA,MACL,aAAA,EACE,QAAA,CAAS,MAAA,KAAW,GAAA,IACnB,QAAA,CAAS,MAAA,KAAW,GAAA,IACnB,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,uBAAuB,CAAA,KAAM;AAAA,KACxD;AAAA,EACF;AACF;AASO,SAAS,oBAAA,CACd,KACA,IAAA,EACQ;AACR,EAAA,OAAO,GAAA,CAAI,OAAA;AAAA,IACT,mDAAA;AAAA,IACA,CAAC,CAAA,EAAG,IAAA,EAAM,KAAA,EAAO,IAAA,KAAS;AACxB,MAAA,OAAO,KAAK,IAAI,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA,EAAI,IAAI,IAAI,IAAI,CAAA,CAAA,CAAA;AAAA,IAC3C;AAAA,GACF;AACF;;;;;"}
@@ -39,9 +39,7 @@ function replaceGithubUrlType(url, type) {
39
39
  return url.replace(
40
40
  /\/\/([^/]+)\/([^/]+)\/([^/]+)\/(blob|tree|edit)\//,
41
41
  (_, host, owner, repo) => {
42
- return `//${host.toLocaleLowerCase("en-US")}/${owner.toLocaleLowerCase(
43
- "en-US"
44
- )}/${repo.toLocaleLowerCase("en-US")}/${type}/`;
42
+ return `//${host}/${owner}/${repo}/${type}/`;
45
43
  }
46
44
  );
47
45
  }
@@ -1 +1 @@
1
- {"version":3,"file":"GithubIntegration.esm.js","sources":["../../src/github/GithubIntegration.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { basicIntegrations, defaultScmResolveUrl } from '../helpers';\nimport {\n RateLimitInfo,\n ScmIntegration,\n ScmIntegrationsFactory,\n} from '../types';\nimport {\n GithubIntegrationConfig,\n readGithubIntegrationConfigs,\n} from './config';\nimport { ConsumedResponse } from '@backstage/errors';\n\n/**\n * A GitHub based integration.\n *\n * @public\n */\nexport class GithubIntegration implements ScmIntegration {\n static factory: ScmIntegrationsFactory<GithubIntegration> = ({ config }) => {\n const configs = readGithubIntegrationConfigs(\n config.getOptionalConfigArray('integrations.github') ?? [],\n );\n return basicIntegrations(\n configs.map(c => new GithubIntegration(c)),\n i => i.config.host,\n );\n };\n\n constructor(private readonly integrationConfig: GithubIntegrationConfig) {}\n\n get type(): string {\n return 'github';\n }\n\n get title(): string {\n return this.integrationConfig.host;\n }\n\n get config(): GithubIntegrationConfig {\n return this.integrationConfig;\n }\n\n resolveUrl(options: {\n url: string;\n base: string;\n lineNumber?: number;\n }): string {\n // GitHub uses blob URLs for files and tree urls for directory listings. But\n // there is a redirect from tree to blob for files, so we can always return\n // tree urls here.\n return replaceGithubUrlType(defaultScmResolveUrl(options), 'tree');\n }\n\n resolveEditUrl(url: string): string {\n return replaceGithubUrlType(url, 'edit');\n }\n\n parseRateLimitInfo(response: ConsumedResponse): RateLimitInfo {\n return {\n isRateLimited:\n response.status === 429 ||\n (response.status === 403 &&\n response.headers.get('x-ratelimit-remaining') === '0'),\n };\n }\n}\n\n/**\n * Takes a GitHub URL and replaces the type part (blob, tree etc).\n *\n * @param url - The original URL\n * @param type - The desired type, e.g. \"blob\"\n * @public\n */\nexport function replaceGithubUrlType(\n url: string,\n type: 'blob' | 'tree' | 'edit',\n): string {\n return url.replace(\n /\\/\\/([^/]+)\\/([^/]+)\\/([^/]+)\\/(blob|tree|edit)\\//,\n (_, host, owner, repo) => {\n return `//${host.toLocaleLowerCase('en-US')}/${owner.toLocaleLowerCase(\n 'en-US',\n )}/${repo.toLocaleLowerCase('en-US')}/${type}/`;\n },\n );\n}\n"],"names":[],"mappings":";;;AAiCO,MAAM,iBAAA,CAA4C;AAAA,EAWvD,YAA6B,iBAAA,EAA4C;AAA5C,IAAA,IAAA,CAAA,iBAAA,GAAA,iBAAA;AAAA,EAA6C;AAAA,EAV1E,OAAO,OAAA,GAAqD,CAAC,EAAE,QAAO,KAAM;AAC1E,IAAA,MAAM,OAAA,GAAU,4BAAA;AAAA,MACd,MAAA,CAAO,sBAAA,CAAuB,qBAAqB,CAAA,IAAK;AAAC,KAC3D;AACA,IAAA,OAAO,iBAAA;AAAA,MACL,QAAQ,GAAA,CAAI,CAAA,CAAA,KAAK,IAAI,iBAAA,CAAkB,CAAC,CAAC,CAAA;AAAA,MACzC,CAAA,CAAA,KAAK,EAAE,MAAA,CAAO;AAAA,KAChB;AAAA,EACF,CAAA;AAAA,EAIA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,QAAA;AAAA,EACT;AAAA,EAEA,IAAI,KAAA,GAAgB;AAClB,IAAA,OAAO,KAAK,iBAAA,CAAkB,IAAA;AAAA,EAChC;AAAA,EAEA,IAAI,MAAA,GAAkC;AACpC,IAAA,OAAO,IAAA,CAAK,iBAAA;AAAA,EACd;AAAA,EAEA,WAAW,OAAA,EAIA;AAIT,IAAA,OAAO,oBAAA,CAAqB,oBAAA,CAAqB,OAAO,CAAA,EAAG,MAAM,CAAA;AAAA,EACnE;AAAA,EAEA,eAAe,GAAA,EAAqB;AAClC,IAAA,OAAO,oBAAA,CAAqB,KAAK,MAAM,CAAA;AAAA,EACzC;AAAA,EAEA,mBAAmB,QAAA,EAA2C;AAC5D,IAAA,OAAO;AAAA,MACL,aAAA,EACE,QAAA,CAAS,MAAA,KAAW,GAAA,IACnB,QAAA,CAAS,MAAA,KAAW,GAAA,IACnB,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,uBAAuB,CAAA,KAAM;AAAA,KACxD;AAAA,EACF;AACF;AASO,SAAS,oBAAA,CACd,KACA,IAAA,EACQ;AACR,EAAA,OAAO,GAAA,CAAI,OAAA;AAAA,IACT,mDAAA;AAAA,IACA,CAAC,CAAA,EAAG,IAAA,EAAM,KAAA,EAAO,IAAA,KAAS;AACxB,MAAA,OAAO,KAAK,IAAA,CAAK,iBAAA,CAAkB,OAAO,CAAC,IAAI,KAAA,CAAM,iBAAA;AAAA,QACnD;AAAA,OACD,CAAA,CAAA,EAAI,IAAA,CAAK,kBAAkB,OAAO,CAAC,IAAI,IAAI,CAAA,CAAA,CAAA;AAAA,IAC9C;AAAA,GACF;AACF;;;;"}
1
+ {"version":3,"file":"GithubIntegration.esm.js","sources":["../../src/github/GithubIntegration.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { basicIntegrations, defaultScmResolveUrl } from '../helpers';\nimport {\n RateLimitInfo,\n ScmIntegration,\n ScmIntegrationsFactory,\n} from '../types';\nimport {\n GithubIntegrationConfig,\n readGithubIntegrationConfigs,\n} from './config';\nimport { ConsumedResponse } from '@backstage/errors';\n\n/**\n * A GitHub based integration.\n *\n * @public\n */\nexport class GithubIntegration implements ScmIntegration {\n static factory: ScmIntegrationsFactory<GithubIntegration> = ({ config }) => {\n const configs = readGithubIntegrationConfigs(\n config.getOptionalConfigArray('integrations.github') ?? [],\n );\n return basicIntegrations(\n configs.map(c => new GithubIntegration(c)),\n i => i.config.host,\n );\n };\n\n constructor(private readonly integrationConfig: GithubIntegrationConfig) {}\n\n get type(): string {\n return 'github';\n }\n\n get title(): string {\n return this.integrationConfig.host;\n }\n\n get config(): GithubIntegrationConfig {\n return this.integrationConfig;\n }\n\n resolveUrl(options: {\n url: string;\n base: string;\n lineNumber?: number;\n }): string {\n // GitHub uses blob URLs for files and tree urls for directory listings. But\n // there is a redirect from tree to blob for files, so we can always return\n // tree urls here.\n return replaceGithubUrlType(defaultScmResolveUrl(options), 'tree');\n }\n\n resolveEditUrl(url: string): string {\n return replaceGithubUrlType(url, 'edit');\n }\n\n parseRateLimitInfo(response: ConsumedResponse): RateLimitInfo {\n return {\n isRateLimited:\n response.status === 429 ||\n (response.status === 403 &&\n response.headers.get('x-ratelimit-remaining') === '0'),\n };\n }\n}\n\n/**\n * Takes a GitHub URL and replaces the type part (blob, tree etc).\n *\n * @param url - The original URL\n * @param type - The desired type, e.g. \"blob\"\n * @public\n */\nexport function replaceGithubUrlType(\n url: string,\n type: 'blob' | 'tree' | 'edit',\n): string {\n return url.replace(\n /\\/\\/([^/]+)\\/([^/]+)\\/([^/]+)\\/(blob|tree|edit)\\//,\n (_, host, owner, repo) => {\n return `//${host}/${owner}/${repo}/${type}/`;\n },\n );\n}\n"],"names":[],"mappings":";;;AAiCO,MAAM,iBAAA,CAA4C;AAAA,EAWvD,YAA6B,iBAAA,EAA4C;AAA5C,IAAA,IAAA,CAAA,iBAAA,GAAA,iBAAA;AAAA,EAA6C;AAAA,EAV1E,OAAO,OAAA,GAAqD,CAAC,EAAE,QAAO,KAAM;AAC1E,IAAA,MAAM,OAAA,GAAU,4BAAA;AAAA,MACd,MAAA,CAAO,sBAAA,CAAuB,qBAAqB,CAAA,IAAK;AAAC,KAC3D;AACA,IAAA,OAAO,iBAAA;AAAA,MACL,QAAQ,GAAA,CAAI,CAAA,CAAA,KAAK,IAAI,iBAAA,CAAkB,CAAC,CAAC,CAAA;AAAA,MACzC,CAAA,CAAA,KAAK,EAAE,MAAA,CAAO;AAAA,KAChB;AAAA,EACF,CAAA;AAAA,EAIA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,QAAA;AAAA,EACT;AAAA,EAEA,IAAI,KAAA,GAAgB;AAClB,IAAA,OAAO,KAAK,iBAAA,CAAkB,IAAA;AAAA,EAChC;AAAA,EAEA,IAAI,MAAA,GAAkC;AACpC,IAAA,OAAO,IAAA,CAAK,iBAAA;AAAA,EACd;AAAA,EAEA,WAAW,OAAA,EAIA;AAIT,IAAA,OAAO,oBAAA,CAAqB,oBAAA,CAAqB,OAAO,CAAA,EAAG,MAAM,CAAA;AAAA,EACnE;AAAA,EAEA,eAAe,GAAA,EAAqB;AAClC,IAAA,OAAO,oBAAA,CAAqB,KAAK,MAAM,CAAA;AAAA,EACzC;AAAA,EAEA,mBAAmB,QAAA,EAA2C;AAC5D,IAAA,OAAO;AAAA,MACL,aAAA,EACE,QAAA,CAAS,MAAA,KAAW,GAAA,IACnB,QAAA,CAAS,MAAA,KAAW,GAAA,IACnB,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,uBAAuB,CAAA,KAAM;AAAA,KACxD;AAAA,EACF;AACF;AASO,SAAS,oBAAA,CACd,KACA,IAAA,EACQ;AACR,EAAA,OAAO,GAAA,CAAI,OAAA;AAAA,IACT,mDAAA;AAAA,IACA,CAAC,CAAA,EAAG,IAAA,EAAM,KAAA,EAAO,IAAA,KAAS;AACxB,MAAA,OAAO,KAAK,IAAI,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA,EAAI,IAAI,IAAI,IAAI,CAAA,CAAA,CAAA;AAAA,IAC3C;AAAA,GACF;AACF;;;;"}
@@ -46,6 +46,7 @@ class GithubAppManager {
46
46
  cache = new Cache();
47
47
  allowedInstallationOwners;
48
48
  // undefined allows all installations
49
+ publicAccess;
49
50
  constructor(config, baseUrl) {
50
51
  this.allowedInstallationOwners = config.allowedInstallationOwners?.map(
51
52
  (owner) => owner.toLocaleLowerCase("en-US")
@@ -61,6 +62,7 @@ class GithubAppManager {
61
62
  authStrategy: authApp.createAppAuth,
62
63
  auth: this.baseAuthConfig
63
64
  });
65
+ this.publicAccess = config.publicAccess ?? false;
64
66
  }
65
67
  async getInstallationCredentials(owner, repo) {
66
68
  if (this.allowedInstallationOwners) {
@@ -100,6 +102,26 @@ class GithubAppManager {
100
102
  };
101
103
  });
102
104
  }
105
+ async getPublicInstallationToken() {
106
+ const [installation] = await this.getInstallations();
107
+ if (!installation) {
108
+ throw new Error(`No installation found for public app`);
109
+ }
110
+ return this.cache.getOrCreateToken(
111
+ `public:${installation.id}`,
112
+ void 0,
113
+ async () => {
114
+ const result = await this.appClient.apps.createInstallationAccessToken({
115
+ installation_id: installation.id,
116
+ headers: HEADERS
117
+ });
118
+ return {
119
+ token: result.data.token,
120
+ expiresAt: luxon.DateTime.fromISO(result.data.expires_at)
121
+ };
122
+ }
123
+ );
124
+ }
103
125
  getInstallations() {
104
126
  return this.appClient.paginate(this.appClient.apps.listInstallations);
105
127
  }
@@ -153,6 +175,16 @@ class GithubAppCredentialsMux {
153
175
  if (result) {
154
176
  return result.credentials.accessToken;
155
177
  }
178
+ const publicAccessApp = this.apps.find((app) => app.publicAccess);
179
+ if (publicAccessApp) {
180
+ const publicResult = await publicAccessApp.getPublicInstallationToken().then(
181
+ (credentials) => ({ credentials, error: void 0 }),
182
+ (error) => ({ credentials: void 0, error })
183
+ );
184
+ if (publicResult.credentials?.accessToken) {
185
+ return publicResult.credentials.accessToken;
186
+ }
187
+ }
156
188
  const errors = results.map((r) => r.error);
157
189
  const notNotFoundError = errors.find((err) => err?.name !== "NotFoundError");
158
190
  if (notNotFoundError) {
@@ -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\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 }\n\n async getInstallationCredentials(\n owner: string,\n repo?: string,\n ): Promise<{ accessToken: string | undefined }> {\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 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 if (installation) {\n return {\n installationId: installation.id,\n suspended: Boolean(installation.suspended_by),\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(owner: string, repo?: string): 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 if (result) {\n return result.credentials!.accessToken;\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 {\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 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(owner: string, repo?: string): 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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"}
@@ -40,6 +40,7 @@ class GithubAppManager {
40
40
  cache = new Cache();
41
41
  allowedInstallationOwners;
42
42
  // undefined allows all installations
43
+ publicAccess;
43
44
  constructor(config, baseUrl) {
44
45
  this.allowedInstallationOwners = config.allowedInstallationOwners?.map(
45
46
  (owner) => owner.toLocaleLowerCase("en-US")
@@ -55,6 +56,7 @@ class GithubAppManager {
55
56
  authStrategy: createAppAuth,
56
57
  auth: this.baseAuthConfig
57
58
  });
59
+ this.publicAccess = config.publicAccess ?? false;
58
60
  }
59
61
  async getInstallationCredentials(owner, repo) {
60
62
  if (this.allowedInstallationOwners) {
@@ -94,6 +96,26 @@ class GithubAppManager {
94
96
  };
95
97
  });
96
98
  }
99
+ async getPublicInstallationToken() {
100
+ const [installation] = await this.getInstallations();
101
+ if (!installation) {
102
+ throw new Error(`No installation found for public app`);
103
+ }
104
+ return this.cache.getOrCreateToken(
105
+ `public:${installation.id}`,
106
+ void 0,
107
+ async () => {
108
+ const result = await this.appClient.apps.createInstallationAccessToken({
109
+ installation_id: installation.id,
110
+ headers: HEADERS
111
+ });
112
+ return {
113
+ token: result.data.token,
114
+ expiresAt: DateTime.fromISO(result.data.expires_at)
115
+ };
116
+ }
117
+ );
118
+ }
97
119
  getInstallations() {
98
120
  return this.appClient.paginate(this.appClient.apps.listInstallations);
99
121
  }
@@ -147,6 +169,16 @@ class GithubAppCredentialsMux {
147
169
  if (result) {
148
170
  return result.credentials.accessToken;
149
171
  }
172
+ const publicAccessApp = this.apps.find((app) => app.publicAccess);
173
+ if (publicAccessApp) {
174
+ const publicResult = await publicAccessApp.getPublicInstallationToken().then(
175
+ (credentials) => ({ credentials, error: void 0 }),
176
+ (error) => ({ credentials: void 0, error })
177
+ );
178
+ if (publicResult.credentials?.accessToken) {
179
+ return publicResult.credentials.accessToken;
180
+ }
181
+ }
150
182
  const errors = results.map((r) => r.error);
151
183
  const notNotFoundError = errors.find((err) => err?.name !== "NotFoundError");
152
184
  if (notNotFoundError) {
@@ -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\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 }\n\n async getInstallationCredentials(\n owner: string,\n repo?: string,\n ): Promise<{ accessToken: string | undefined }> {\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 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 if (installation) {\n return {\n installationId: installation.id,\n suspended: Boolean(installation.suspended_by),\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(owner: string, repo?: string): 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 if (result) {\n return result.credentials!.accessToken;\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 {\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 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(owner: string, repo?: string): 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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"}
@@ -19,7 +19,8 @@ function readGithubIntegrationConfig(config) {
19
19
  privateKey: c.getString("privateKey"),
20
20
  allowedInstallationOwners: c.getOptionalStringArray(
21
21
  "allowedInstallationOwners"
22
- )
22
+ ),
23
+ publicAccess: c.getOptionalBoolean("publicAccess")
23
24
  }));
24
25
  if (!helpers.isValidHost(host)) {
25
26
  throw new Error(
@@ -1 +1 @@
1
- {"version":3,"file":"config.cjs.js","sources":["../../src/github/config.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { Config } from '@backstage/config';\nimport { trimEnd } from 'lodash';\nimport { isValidHost } from '../helpers';\n\nconst GITHUB_HOST = 'github.com';\nconst GITHUB_API_BASE_URL = 'https://api.github.com';\nconst GITHUB_RAW_BASE_URL = 'https://raw.githubusercontent.com';\n\n/**\n * The configuration parameters for a single GitHub integration.\n *\n * @public\n */\nexport type GithubIntegrationConfig = {\n /**\n * The host of the target that this matches on, e.g. \"github.com\"\n */\n host: string;\n\n /**\n * The base URL of the API of this provider, e.g. \"https://api.github.com\",\n * with no trailing slash.\n *\n * May be omitted specifically for GitHub; then it will be deduced.\n *\n * The API will always be preferred if both its base URL and a token are\n * present.\n */\n apiBaseUrl?: string;\n\n /**\n * The base URL of the raw fetch endpoint of this provider, e.g.\n * \"https://raw.githubusercontent.com\", with no trailing slash.\n *\n * May be omitted specifically for GitHub; then it will be deduced.\n *\n * The API will always be preferred if both its base URL and a token are\n * present.\n */\n rawBaseUrl?: string;\n\n /**\n * The authorization token to use for requests to this provider.\n *\n * If no token is specified, anonymous access is used.\n */\n token?: string;\n\n /**\n * The GitHub Apps configuration to use for requests to this provider.\n *\n * If no apps are specified, token or anonymous is used.\n */\n apps?: GithubAppConfig[];\n};\n\n/**\n * The configuration parameters for authenticating a GitHub Application.\n *\n * @remarks\n *\n * A GitHub Apps configuration can be generated using the `backstage-cli create-github-app` command.\n *\n * @public\n */\nexport type GithubAppConfig = {\n /**\n * Unique app identifier, found at https://github.com/organizations/$org/settings/apps/$AppName\n */\n appId: number;\n /**\n * The private key is used by the GitHub App integration to authenticate the app.\n * A private key can be generated from the app at https://github.com/organizations/$org/settings/apps/$AppName\n */\n privateKey: string;\n /**\n * Webhook secret can be configured at https://github.com/organizations/$org/settings/apps/$AppName\n */\n webhookSecret?: string;\n /**\n * Found at https://github.com/organizations/$org/settings/apps/$AppName\n */\n clientId: string;\n /**\n * Client secrets can be generated at https://github.com/organizations/$org/settings/apps/$AppName\n */\n clientSecret: string;\n /**\n * List of installation owners allowed to be used by this GitHub app. The GitHub UI does not provide a way to list the installations.\n * However you can list the installations with the GitHub API. You can find the list of installations here:\n * https://api.github.com/app/installations\n * The relevant documentation for this is here.\n * https://docs.github.com/en/rest/reference/apps#list-installations-for-the-authenticated-app--code-samples\n */\n allowedInstallationOwners?: string[];\n};\n\n/**\n * Reads a single GitHub integration config.\n *\n * @param config - The config object of a single integration\n * @public\n */\nexport function readGithubIntegrationConfig(\n config: Config,\n): GithubIntegrationConfig {\n const host = config.getOptionalString('host') ?? GITHUB_HOST;\n let apiBaseUrl = config.getOptionalString('apiBaseUrl');\n let rawBaseUrl = config.getOptionalString('rawBaseUrl');\n const token = config.getOptionalString('token')?.trim();\n const apps = config.getOptionalConfigArray('apps')?.map(c => ({\n appId: c.getNumber('appId'),\n clientId: c.getString('clientId'),\n clientSecret: c.getString('clientSecret'),\n webhookSecret: c.getOptionalString('webhookSecret'),\n privateKey: c.getString('privateKey'),\n allowedInstallationOwners: c.getOptionalStringArray(\n 'allowedInstallationOwners',\n ),\n }));\n\n if (!isValidHost(host)) {\n throw new Error(\n `Invalid GitHub integration config, '${host}' is not a valid host`,\n );\n }\n\n if (apiBaseUrl) {\n apiBaseUrl = trimEnd(apiBaseUrl, '/');\n } else if (host === GITHUB_HOST) {\n apiBaseUrl = GITHUB_API_BASE_URL;\n }\n\n if (rawBaseUrl) {\n rawBaseUrl = trimEnd(rawBaseUrl, '/');\n } else if (host === GITHUB_HOST) {\n rawBaseUrl = GITHUB_RAW_BASE_URL;\n }\n\n return { host, apiBaseUrl, rawBaseUrl, token, apps };\n}\n\n/**\n * Reads a set of GitHub integration configs, and inserts some defaults for\n * public GitHub if not specified.\n *\n * @param configs - All of the integration config objects\n * @public\n */\nexport function readGithubIntegrationConfigs(\n configs: Config[],\n): GithubIntegrationConfig[] {\n // First read all the explicit integrations\n const result = configs.map(readGithubIntegrationConfig);\n\n // If no explicit github.com integration was added, put one in the list as\n // a convenience\n if (!result.some(c => c.host === GITHUB_HOST)) {\n result.push({\n host: GITHUB_HOST,\n apiBaseUrl: GITHUB_API_BASE_URL,\n rawBaseUrl: GITHUB_RAW_BASE_URL,\n });\n }\n\n return result;\n}\n"],"names":["isValidHost","trimEnd"],"mappings":";;;;;AAoBA,MAAM,WAAA,GAAc,YAAA;AACpB,MAAM,mBAAA,GAAsB,wBAAA;AAC5B,MAAM,mBAAA,GAAsB,mCAAA;AAiGrB,SAAS,4BACd,MAAA,EACyB;AACzB,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,iBAAA,CAAkB,MAAM,CAAA,IAAK,WAAA;AACjD,EAAA,IAAI,UAAA,GAAa,MAAA,CAAO,iBAAA,CAAkB,YAAY,CAAA;AACtD,EAAA,IAAI,UAAA,GAAa,MAAA,CAAO,iBAAA,CAAkB,YAAY,CAAA;AACtD,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,iBAAA,CAAkB,OAAO,GAAG,IAAA,EAAK;AACtD,EAAA,MAAM,OAAO,MAAA,CAAO,sBAAA,CAAuB,MAAM,CAAA,EAAG,IAAI,CAAA,CAAA,MAAM;AAAA,IAC5D,KAAA,EAAO,CAAA,CAAE,SAAA,CAAU,OAAO,CAAA;AAAA,IAC1B,QAAA,EAAU,CAAA,CAAE,SAAA,CAAU,UAAU,CAAA;AAAA,IAChC,YAAA,EAAc,CAAA,CAAE,SAAA,CAAU,cAAc,CAAA;AAAA,IACxC,aAAA,EAAe,CAAA,CAAE,iBAAA,CAAkB,eAAe,CAAA;AAAA,IAClD,UAAA,EAAY,CAAA,CAAE,SAAA,CAAU,YAAY,CAAA;AAAA,IACpC,2BAA2B,CAAA,CAAE,sBAAA;AAAA,MAC3B;AAAA;AACF,GACF,CAAE,CAAA;AAEF,EAAA,IAAI,CAACA,mBAAA,CAAY,IAAI,CAAA,EAAG;AACtB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,uCAAuC,IAAI,CAAA,qBAAA;AAAA,KAC7C;AAAA,EACF;AAEA,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,UAAA,GAAaC,cAAA,CAAQ,YAAY,GAAG,CAAA;AAAA,EACtC,CAAA,MAAA,IAAW,SAAS,WAAA,EAAa;AAC/B,IAAA,UAAA,GAAa,mBAAA;AAAA,EACf;AAEA,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,UAAA,GAAaA,cAAA,CAAQ,YAAY,GAAG,CAAA;AAAA,EACtC,CAAA,MAAA,IAAW,SAAS,WAAA,EAAa;AAC/B,IAAA,UAAA,GAAa,mBAAA;AAAA,EACf;AAEA,EAAA,OAAO,EAAE,IAAA,EAAM,UAAA,EAAY,UAAA,EAAY,OAAO,IAAA,EAAK;AACrD;AASO,SAAS,6BACd,OAAA,EAC2B;AAE3B,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,2BAA2B,CAAA;AAItD,EAAA,IAAI,CAAC,MAAA,CAAO,IAAA,CAAK,OAAK,CAAA,CAAE,IAAA,KAAS,WAAW,CAAA,EAAG;AAC7C,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,IAAA,EAAM,WAAA;AAAA,MACN,UAAA,EAAY,mBAAA;AAAA,MACZ,UAAA,EAAY;AAAA,KACb,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,MAAA;AACT;;;;;"}
1
+ {"version":3,"file":"config.cjs.js","sources":["../../src/github/config.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { Config } from '@backstage/config';\nimport { trimEnd } from 'lodash';\nimport { isValidHost } from '../helpers';\n\nconst GITHUB_HOST = 'github.com';\nconst GITHUB_API_BASE_URL = 'https://api.github.com';\nconst GITHUB_RAW_BASE_URL = 'https://raw.githubusercontent.com';\n\n/**\n * The configuration parameters for a single GitHub integration.\n *\n * @public\n */\nexport type GithubIntegrationConfig = {\n /**\n * The host of the target that this matches on, e.g. \"github.com\"\n */\n host: string;\n\n /**\n * The base URL of the API of this provider, e.g. \"https://api.github.com\",\n * with no trailing slash.\n *\n * May be omitted specifically for GitHub; then it will be deduced.\n *\n * The API will always be preferred if both its base URL and a token are\n * present.\n */\n apiBaseUrl?: string;\n\n /**\n * The base URL of the raw fetch endpoint of this provider, e.g.\n * \"https://raw.githubusercontent.com\", with no trailing slash.\n *\n * May be omitted specifically for GitHub; then it will be deduced.\n *\n * The API will always be preferred if both its base URL and a token are\n * present.\n */\n rawBaseUrl?: string;\n\n /**\n * The authorization token to use for requests to this provider.\n *\n * If no token is specified, anonymous access is used.\n */\n token?: string;\n\n /**\n * The GitHub Apps configuration to use for requests to this provider.\n *\n * If no apps are specified, token or anonymous is used.\n */\n apps?: GithubAppConfig[];\n};\n\n/**\n * The configuration parameters for authenticating a GitHub Application.\n *\n * @remarks\n *\n * A GitHub Apps configuration can be generated using the `backstage-cli create-github-app` command.\n *\n * @public\n */\nexport type GithubAppConfig = {\n /**\n * Unique app identifier, found at https://github.com/organizations/$org/settings/apps/$AppName\n */\n appId: number;\n /**\n * The private key is used by the GitHub App integration to authenticate the app.\n * A private key can be generated from the app at https://github.com/organizations/$org/settings/apps/$AppName\n */\n privateKey: string;\n /**\n * Webhook secret can be configured at https://github.com/organizations/$org/settings/apps/$AppName\n */\n webhookSecret?: string;\n /**\n * Found at https://github.com/organizations/$org/settings/apps/$AppName\n */\n clientId: string;\n /**\n * Client secrets can be generated at https://github.com/organizations/$org/settings/apps/$AppName\n */\n clientSecret: string;\n /**\n * List of installation owners allowed to be used by this GitHub app. The GitHub UI does not provide a way to list the installations.\n * However you can list the installations with the GitHub API. You can find the list of installations here:\n * https://api.github.com/app/installations\n * The relevant documentation for this is here.\n * https://docs.github.com/en/rest/reference/apps#list-installations-for-the-authenticated-app--code-samples\n */\n allowedInstallationOwners?: string[];\n /**\n * If true, then an installation token will be issued for access when no other token is available.\n */\n publicAccess?: boolean;\n};\n\n/**\n * Reads a single GitHub integration config.\n *\n * @param config - The config object of a single integration\n * @public\n */\nexport function readGithubIntegrationConfig(\n config: Config,\n): GithubIntegrationConfig {\n const host = config.getOptionalString('host') ?? GITHUB_HOST;\n let apiBaseUrl = config.getOptionalString('apiBaseUrl');\n let rawBaseUrl = config.getOptionalString('rawBaseUrl');\n const token = config.getOptionalString('token')?.trim();\n const apps = config.getOptionalConfigArray('apps')?.map(c => ({\n appId: c.getNumber('appId'),\n clientId: c.getString('clientId'),\n clientSecret: c.getString('clientSecret'),\n webhookSecret: c.getOptionalString('webhookSecret'),\n privateKey: c.getString('privateKey'),\n allowedInstallationOwners: c.getOptionalStringArray(\n 'allowedInstallationOwners',\n ),\n publicAccess: c.getOptionalBoolean('publicAccess'),\n }));\n\n if (!isValidHost(host)) {\n throw new Error(\n `Invalid GitHub integration config, '${host}' is not a valid host`,\n );\n }\n\n if (apiBaseUrl) {\n apiBaseUrl = trimEnd(apiBaseUrl, '/');\n } else if (host === GITHUB_HOST) {\n apiBaseUrl = GITHUB_API_BASE_URL;\n }\n\n if (rawBaseUrl) {\n rawBaseUrl = trimEnd(rawBaseUrl, '/');\n } else if (host === GITHUB_HOST) {\n rawBaseUrl = GITHUB_RAW_BASE_URL;\n }\n\n return { host, apiBaseUrl, rawBaseUrl, token, apps };\n}\n\n/**\n * Reads a set of GitHub integration configs, and inserts some defaults for\n * public GitHub if not specified.\n *\n * @param configs - All of the integration config objects\n * @public\n */\nexport function readGithubIntegrationConfigs(\n configs: Config[],\n): GithubIntegrationConfig[] {\n // First read all the explicit integrations\n const result = configs.map(readGithubIntegrationConfig);\n\n // If no explicit github.com integration was added, put one in the list as\n // a convenience\n if (!result.some(c => c.host === GITHUB_HOST)) {\n result.push({\n host: GITHUB_HOST,\n apiBaseUrl: GITHUB_API_BASE_URL,\n rawBaseUrl: GITHUB_RAW_BASE_URL,\n });\n }\n\n return result;\n}\n"],"names":["isValidHost","trimEnd"],"mappings":";;;;;AAoBA,MAAM,WAAA,GAAc,YAAA;AACpB,MAAM,mBAAA,GAAsB,wBAAA;AAC5B,MAAM,mBAAA,GAAsB,mCAAA;AAqGrB,SAAS,4BACd,MAAA,EACyB;AACzB,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,iBAAA,CAAkB,MAAM,CAAA,IAAK,WAAA;AACjD,EAAA,IAAI,UAAA,GAAa,MAAA,CAAO,iBAAA,CAAkB,YAAY,CAAA;AACtD,EAAA,IAAI,UAAA,GAAa,MAAA,CAAO,iBAAA,CAAkB,YAAY,CAAA;AACtD,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,iBAAA,CAAkB,OAAO,GAAG,IAAA,EAAK;AACtD,EAAA,MAAM,OAAO,MAAA,CAAO,sBAAA,CAAuB,MAAM,CAAA,EAAG,IAAI,CAAA,CAAA,MAAM;AAAA,IAC5D,KAAA,EAAO,CAAA,CAAE,SAAA,CAAU,OAAO,CAAA;AAAA,IAC1B,QAAA,EAAU,CAAA,CAAE,SAAA,CAAU,UAAU,CAAA;AAAA,IAChC,YAAA,EAAc,CAAA,CAAE,SAAA,CAAU,cAAc,CAAA;AAAA,IACxC,aAAA,EAAe,CAAA,CAAE,iBAAA,CAAkB,eAAe,CAAA;AAAA,IAClD,UAAA,EAAY,CAAA,CAAE,SAAA,CAAU,YAAY,CAAA;AAAA,IACpC,2BAA2B,CAAA,CAAE,sBAAA;AAAA,MAC3B;AAAA,KACF;AAAA,IACA,YAAA,EAAc,CAAA,CAAE,kBAAA,CAAmB,cAAc;AAAA,GACnD,CAAE,CAAA;AAEF,EAAA,IAAI,CAACA,mBAAA,CAAY,IAAI,CAAA,EAAG;AACtB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,uCAAuC,IAAI,CAAA,qBAAA;AAAA,KAC7C;AAAA,EACF;AAEA,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,UAAA,GAAaC,cAAA,CAAQ,YAAY,GAAG,CAAA;AAAA,EACtC,CAAA,MAAA,IAAW,SAAS,WAAA,EAAa;AAC/B,IAAA,UAAA,GAAa,mBAAA;AAAA,EACf;AAEA,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,UAAA,GAAaA,cAAA,CAAQ,YAAY,GAAG,CAAA;AAAA,EACtC,CAAA,MAAA,IAAW,SAAS,WAAA,EAAa;AAC/B,IAAA,UAAA,GAAa,mBAAA;AAAA,EACf;AAEA,EAAA,OAAO,EAAE,IAAA,EAAM,UAAA,EAAY,UAAA,EAAY,OAAO,IAAA,EAAK;AACrD;AASO,SAAS,6BACd,OAAA,EAC2B;AAE3B,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,2BAA2B,CAAA;AAItD,EAAA,IAAI,CAAC,MAAA,CAAO,IAAA,CAAK,OAAK,CAAA,CAAE,IAAA,KAAS,WAAW,CAAA,EAAG;AAC7C,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,IAAA,EAAM,WAAA;AAAA,MACN,UAAA,EAAY,mBAAA;AAAA,MACZ,UAAA,EAAY;AAAA,KACb,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,MAAA;AACT;;;;;"}
@@ -17,7 +17,8 @@ function readGithubIntegrationConfig(config) {
17
17
  privateKey: c.getString("privateKey"),
18
18
  allowedInstallationOwners: c.getOptionalStringArray(
19
19
  "allowedInstallationOwners"
20
- )
20
+ ),
21
+ publicAccess: c.getOptionalBoolean("publicAccess")
21
22
  }));
22
23
  if (!isValidHost(host)) {
23
24
  throw new Error(
@@ -1 +1 @@
1
- {"version":3,"file":"config.esm.js","sources":["../../src/github/config.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { Config } from '@backstage/config';\nimport { trimEnd } from 'lodash';\nimport { isValidHost } from '../helpers';\n\nconst GITHUB_HOST = 'github.com';\nconst GITHUB_API_BASE_URL = 'https://api.github.com';\nconst GITHUB_RAW_BASE_URL = 'https://raw.githubusercontent.com';\n\n/**\n * The configuration parameters for a single GitHub integration.\n *\n * @public\n */\nexport type GithubIntegrationConfig = {\n /**\n * The host of the target that this matches on, e.g. \"github.com\"\n */\n host: string;\n\n /**\n * The base URL of the API of this provider, e.g. \"https://api.github.com\",\n * with no trailing slash.\n *\n * May be omitted specifically for GitHub; then it will be deduced.\n *\n * The API will always be preferred if both its base URL and a token are\n * present.\n */\n apiBaseUrl?: string;\n\n /**\n * The base URL of the raw fetch endpoint of this provider, e.g.\n * \"https://raw.githubusercontent.com\", with no trailing slash.\n *\n * May be omitted specifically for GitHub; then it will be deduced.\n *\n * The API will always be preferred if both its base URL and a token are\n * present.\n */\n rawBaseUrl?: string;\n\n /**\n * The authorization token to use for requests to this provider.\n *\n * If no token is specified, anonymous access is used.\n */\n token?: string;\n\n /**\n * The GitHub Apps configuration to use for requests to this provider.\n *\n * If no apps are specified, token or anonymous is used.\n */\n apps?: GithubAppConfig[];\n};\n\n/**\n * The configuration parameters for authenticating a GitHub Application.\n *\n * @remarks\n *\n * A GitHub Apps configuration can be generated using the `backstage-cli create-github-app` command.\n *\n * @public\n */\nexport type GithubAppConfig = {\n /**\n * Unique app identifier, found at https://github.com/organizations/$org/settings/apps/$AppName\n */\n appId: number;\n /**\n * The private key is used by the GitHub App integration to authenticate the app.\n * A private key can be generated from the app at https://github.com/organizations/$org/settings/apps/$AppName\n */\n privateKey: string;\n /**\n * Webhook secret can be configured at https://github.com/organizations/$org/settings/apps/$AppName\n */\n webhookSecret?: string;\n /**\n * Found at https://github.com/organizations/$org/settings/apps/$AppName\n */\n clientId: string;\n /**\n * Client secrets can be generated at https://github.com/organizations/$org/settings/apps/$AppName\n */\n clientSecret: string;\n /**\n * List of installation owners allowed to be used by this GitHub app. The GitHub UI does not provide a way to list the installations.\n * However you can list the installations with the GitHub API. You can find the list of installations here:\n * https://api.github.com/app/installations\n * The relevant documentation for this is here.\n * https://docs.github.com/en/rest/reference/apps#list-installations-for-the-authenticated-app--code-samples\n */\n allowedInstallationOwners?: string[];\n};\n\n/**\n * Reads a single GitHub integration config.\n *\n * @param config - The config object of a single integration\n * @public\n */\nexport function readGithubIntegrationConfig(\n config: Config,\n): GithubIntegrationConfig {\n const host = config.getOptionalString('host') ?? GITHUB_HOST;\n let apiBaseUrl = config.getOptionalString('apiBaseUrl');\n let rawBaseUrl = config.getOptionalString('rawBaseUrl');\n const token = config.getOptionalString('token')?.trim();\n const apps = config.getOptionalConfigArray('apps')?.map(c => ({\n appId: c.getNumber('appId'),\n clientId: c.getString('clientId'),\n clientSecret: c.getString('clientSecret'),\n webhookSecret: c.getOptionalString('webhookSecret'),\n privateKey: c.getString('privateKey'),\n allowedInstallationOwners: c.getOptionalStringArray(\n 'allowedInstallationOwners',\n ),\n }));\n\n if (!isValidHost(host)) {\n throw new Error(\n `Invalid GitHub integration config, '${host}' is not a valid host`,\n );\n }\n\n if (apiBaseUrl) {\n apiBaseUrl = trimEnd(apiBaseUrl, '/');\n } else if (host === GITHUB_HOST) {\n apiBaseUrl = GITHUB_API_BASE_URL;\n }\n\n if (rawBaseUrl) {\n rawBaseUrl = trimEnd(rawBaseUrl, '/');\n } else if (host === GITHUB_HOST) {\n rawBaseUrl = GITHUB_RAW_BASE_URL;\n }\n\n return { host, apiBaseUrl, rawBaseUrl, token, apps };\n}\n\n/**\n * Reads a set of GitHub integration configs, and inserts some defaults for\n * public GitHub if not specified.\n *\n * @param configs - All of the integration config objects\n * @public\n */\nexport function readGithubIntegrationConfigs(\n configs: Config[],\n): GithubIntegrationConfig[] {\n // First read all the explicit integrations\n const result = configs.map(readGithubIntegrationConfig);\n\n // If no explicit github.com integration was added, put one in the list as\n // a convenience\n if (!result.some(c => c.host === GITHUB_HOST)) {\n result.push({\n host: GITHUB_HOST,\n apiBaseUrl: GITHUB_API_BASE_URL,\n rawBaseUrl: GITHUB_RAW_BASE_URL,\n });\n }\n\n return result;\n}\n"],"names":[],"mappings":";;;AAoBA,MAAM,WAAA,GAAc,YAAA;AACpB,MAAM,mBAAA,GAAsB,wBAAA;AAC5B,MAAM,mBAAA,GAAsB,mCAAA;AAiGrB,SAAS,4BACd,MAAA,EACyB;AACzB,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,iBAAA,CAAkB,MAAM,CAAA,IAAK,WAAA;AACjD,EAAA,IAAI,UAAA,GAAa,MAAA,CAAO,iBAAA,CAAkB,YAAY,CAAA;AACtD,EAAA,IAAI,UAAA,GAAa,MAAA,CAAO,iBAAA,CAAkB,YAAY,CAAA;AACtD,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,iBAAA,CAAkB,OAAO,GAAG,IAAA,EAAK;AACtD,EAAA,MAAM,OAAO,MAAA,CAAO,sBAAA,CAAuB,MAAM,CAAA,EAAG,IAAI,CAAA,CAAA,MAAM;AAAA,IAC5D,KAAA,EAAO,CAAA,CAAE,SAAA,CAAU,OAAO,CAAA;AAAA,IAC1B,QAAA,EAAU,CAAA,CAAE,SAAA,CAAU,UAAU,CAAA;AAAA,IAChC,YAAA,EAAc,CAAA,CAAE,SAAA,CAAU,cAAc,CAAA;AAAA,IACxC,aAAA,EAAe,CAAA,CAAE,iBAAA,CAAkB,eAAe,CAAA;AAAA,IAClD,UAAA,EAAY,CAAA,CAAE,SAAA,CAAU,YAAY,CAAA;AAAA,IACpC,2BAA2B,CAAA,CAAE,sBAAA;AAAA,MAC3B;AAAA;AACF,GACF,CAAE,CAAA;AAEF,EAAA,IAAI,CAAC,WAAA,CAAY,IAAI,CAAA,EAAG;AACtB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,uCAAuC,IAAI,CAAA,qBAAA;AAAA,KAC7C;AAAA,EACF;AAEA,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,UAAA,GAAa,OAAA,CAAQ,YAAY,GAAG,CAAA;AAAA,EACtC,CAAA,MAAA,IAAW,SAAS,WAAA,EAAa;AAC/B,IAAA,UAAA,GAAa,mBAAA;AAAA,EACf;AAEA,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,UAAA,GAAa,OAAA,CAAQ,YAAY,GAAG,CAAA;AAAA,EACtC,CAAA,MAAA,IAAW,SAAS,WAAA,EAAa;AAC/B,IAAA,UAAA,GAAa,mBAAA;AAAA,EACf;AAEA,EAAA,OAAO,EAAE,IAAA,EAAM,UAAA,EAAY,UAAA,EAAY,OAAO,IAAA,EAAK;AACrD;AASO,SAAS,6BACd,OAAA,EAC2B;AAE3B,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,2BAA2B,CAAA;AAItD,EAAA,IAAI,CAAC,MAAA,CAAO,IAAA,CAAK,OAAK,CAAA,CAAE,IAAA,KAAS,WAAW,CAAA,EAAG;AAC7C,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,IAAA,EAAM,WAAA;AAAA,MACN,UAAA,EAAY,mBAAA;AAAA,MACZ,UAAA,EAAY;AAAA,KACb,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,MAAA;AACT;;;;"}
1
+ {"version":3,"file":"config.esm.js","sources":["../../src/github/config.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { Config } from '@backstage/config';\nimport { trimEnd } from 'lodash';\nimport { isValidHost } from '../helpers';\n\nconst GITHUB_HOST = 'github.com';\nconst GITHUB_API_BASE_URL = 'https://api.github.com';\nconst GITHUB_RAW_BASE_URL = 'https://raw.githubusercontent.com';\n\n/**\n * The configuration parameters for a single GitHub integration.\n *\n * @public\n */\nexport type GithubIntegrationConfig = {\n /**\n * The host of the target that this matches on, e.g. \"github.com\"\n */\n host: string;\n\n /**\n * The base URL of the API of this provider, e.g. \"https://api.github.com\",\n * with no trailing slash.\n *\n * May be omitted specifically for GitHub; then it will be deduced.\n *\n * The API will always be preferred if both its base URL and a token are\n * present.\n */\n apiBaseUrl?: string;\n\n /**\n * The base URL of the raw fetch endpoint of this provider, e.g.\n * \"https://raw.githubusercontent.com\", with no trailing slash.\n *\n * May be omitted specifically for GitHub; then it will be deduced.\n *\n * The API will always be preferred if both its base URL and a token are\n * present.\n */\n rawBaseUrl?: string;\n\n /**\n * The authorization token to use for requests to this provider.\n *\n * If no token is specified, anonymous access is used.\n */\n token?: string;\n\n /**\n * The GitHub Apps configuration to use for requests to this provider.\n *\n * If no apps are specified, token or anonymous is used.\n */\n apps?: GithubAppConfig[];\n};\n\n/**\n * The configuration parameters for authenticating a GitHub Application.\n *\n * @remarks\n *\n * A GitHub Apps configuration can be generated using the `backstage-cli create-github-app` command.\n *\n * @public\n */\nexport type GithubAppConfig = {\n /**\n * Unique app identifier, found at https://github.com/organizations/$org/settings/apps/$AppName\n */\n appId: number;\n /**\n * The private key is used by the GitHub App integration to authenticate the app.\n * A private key can be generated from the app at https://github.com/organizations/$org/settings/apps/$AppName\n */\n privateKey: string;\n /**\n * Webhook secret can be configured at https://github.com/organizations/$org/settings/apps/$AppName\n */\n webhookSecret?: string;\n /**\n * Found at https://github.com/organizations/$org/settings/apps/$AppName\n */\n clientId: string;\n /**\n * Client secrets can be generated at https://github.com/organizations/$org/settings/apps/$AppName\n */\n clientSecret: string;\n /**\n * List of installation owners allowed to be used by this GitHub app. The GitHub UI does not provide a way to list the installations.\n * However you can list the installations with the GitHub API. You can find the list of installations here:\n * https://api.github.com/app/installations\n * The relevant documentation for this is here.\n * https://docs.github.com/en/rest/reference/apps#list-installations-for-the-authenticated-app--code-samples\n */\n allowedInstallationOwners?: string[];\n /**\n * If true, then an installation token will be issued for access when no other token is available.\n */\n publicAccess?: boolean;\n};\n\n/**\n * Reads a single GitHub integration config.\n *\n * @param config - The config object of a single integration\n * @public\n */\nexport function readGithubIntegrationConfig(\n config: Config,\n): GithubIntegrationConfig {\n const host = config.getOptionalString('host') ?? GITHUB_HOST;\n let apiBaseUrl = config.getOptionalString('apiBaseUrl');\n let rawBaseUrl = config.getOptionalString('rawBaseUrl');\n const token = config.getOptionalString('token')?.trim();\n const apps = config.getOptionalConfigArray('apps')?.map(c => ({\n appId: c.getNumber('appId'),\n clientId: c.getString('clientId'),\n clientSecret: c.getString('clientSecret'),\n webhookSecret: c.getOptionalString('webhookSecret'),\n privateKey: c.getString('privateKey'),\n allowedInstallationOwners: c.getOptionalStringArray(\n 'allowedInstallationOwners',\n ),\n publicAccess: c.getOptionalBoolean('publicAccess'),\n }));\n\n if (!isValidHost(host)) {\n throw new Error(\n `Invalid GitHub integration config, '${host}' is not a valid host`,\n );\n }\n\n if (apiBaseUrl) {\n apiBaseUrl = trimEnd(apiBaseUrl, '/');\n } else if (host === GITHUB_HOST) {\n apiBaseUrl = GITHUB_API_BASE_URL;\n }\n\n if (rawBaseUrl) {\n rawBaseUrl = trimEnd(rawBaseUrl, '/');\n } else if (host === GITHUB_HOST) {\n rawBaseUrl = GITHUB_RAW_BASE_URL;\n }\n\n return { host, apiBaseUrl, rawBaseUrl, token, apps };\n}\n\n/**\n * Reads a set of GitHub integration configs, and inserts some defaults for\n * public GitHub if not specified.\n *\n * @param configs - All of the integration config objects\n * @public\n */\nexport function readGithubIntegrationConfigs(\n configs: Config[],\n): GithubIntegrationConfig[] {\n // First read all the explicit integrations\n const result = configs.map(readGithubIntegrationConfig);\n\n // If no explicit github.com integration was added, put one in the list as\n // a convenience\n if (!result.some(c => c.host === GITHUB_HOST)) {\n result.push({\n host: GITHUB_HOST,\n apiBaseUrl: GITHUB_API_BASE_URL,\n rawBaseUrl: GITHUB_RAW_BASE_URL,\n });\n }\n\n return result;\n}\n"],"names":[],"mappings":";;;AAoBA,MAAM,WAAA,GAAc,YAAA;AACpB,MAAM,mBAAA,GAAsB,wBAAA;AAC5B,MAAM,mBAAA,GAAsB,mCAAA;AAqGrB,SAAS,4BACd,MAAA,EACyB;AACzB,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,iBAAA,CAAkB,MAAM,CAAA,IAAK,WAAA;AACjD,EAAA,IAAI,UAAA,GAAa,MAAA,CAAO,iBAAA,CAAkB,YAAY,CAAA;AACtD,EAAA,IAAI,UAAA,GAAa,MAAA,CAAO,iBAAA,CAAkB,YAAY,CAAA;AACtD,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,iBAAA,CAAkB,OAAO,GAAG,IAAA,EAAK;AACtD,EAAA,MAAM,OAAO,MAAA,CAAO,sBAAA,CAAuB,MAAM,CAAA,EAAG,IAAI,CAAA,CAAA,MAAM;AAAA,IAC5D,KAAA,EAAO,CAAA,CAAE,SAAA,CAAU,OAAO,CAAA;AAAA,IAC1B,QAAA,EAAU,CAAA,CAAE,SAAA,CAAU,UAAU,CAAA;AAAA,IAChC,YAAA,EAAc,CAAA,CAAE,SAAA,CAAU,cAAc,CAAA;AAAA,IACxC,aAAA,EAAe,CAAA,CAAE,iBAAA,CAAkB,eAAe,CAAA;AAAA,IAClD,UAAA,EAAY,CAAA,CAAE,SAAA,CAAU,YAAY,CAAA;AAAA,IACpC,2BAA2B,CAAA,CAAE,sBAAA;AAAA,MAC3B;AAAA,KACF;AAAA,IACA,YAAA,EAAc,CAAA,CAAE,kBAAA,CAAmB,cAAc;AAAA,GACnD,CAAE,CAAA;AAEF,EAAA,IAAI,CAAC,WAAA,CAAY,IAAI,CAAA,EAAG;AACtB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,uCAAuC,IAAI,CAAA,qBAAA;AAAA,KAC7C;AAAA,EACF;AAEA,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,UAAA,GAAa,OAAA,CAAQ,YAAY,GAAG,CAAA;AAAA,EACtC,CAAA,MAAA,IAAW,SAAS,WAAA,EAAa;AAC/B,IAAA,UAAA,GAAa,mBAAA;AAAA,EACf;AAEA,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,UAAA,GAAa,OAAA,CAAQ,YAAY,GAAG,CAAA;AAAA,EACtC,CAAA,MAAA,IAAW,SAAS,WAAA,EAAa;AAC/B,IAAA,UAAA,GAAa,mBAAA;AAAA,EACf;AAEA,EAAA,OAAO,EAAE,IAAA,EAAM,UAAA,EAAY,UAAA,EAAY,OAAO,IAAA,EAAK;AACrD;AASO,SAAS,6BACd,OAAA,EAC2B;AAE3B,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,2BAA2B,CAAA;AAItD,EAAA,IAAI,CAAC,MAAA,CAAO,IAAA,CAAK,OAAK,CAAA,CAAE,IAAA,KAAS,WAAW,CAAA,EAAG;AAC7C,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,IAAA,EAAM,WAAA;AAAA,MACN,UAAA,EAAY,mBAAA;AAAA,MACZ,UAAA,EAAY;AAAA,KACb,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,MAAA;AACT;;;;"}
package/dist/index.cjs.js CHANGED
@@ -76,6 +76,7 @@ exports.readBitbucketCloudIntegrationConfigs = config$5.readBitbucketCloudIntegr
76
76
  exports.getBitbucketCloudDefaultBranch = core$2.getBitbucketCloudDefaultBranch;
77
77
  exports.getBitbucketCloudDownloadUrl = core$2.getBitbucketCloudDownloadUrl;
78
78
  exports.getBitbucketCloudFileFetchUrl = core$2.getBitbucketCloudFileFetchUrl;
79
+ exports.getBitbucketCloudOAuthToken = core$2.getBitbucketCloudOAuthToken;
79
80
  exports.getBitbucketCloudRequestOptions = core$2.getBitbucketCloudRequestOptions;
80
81
  exports.BitbucketServerIntegration = BitbucketServerIntegration.BitbucketServerIntegration;
81
82
  exports.readBitbucketServerIntegrationConfig = config$6.readBitbucketServerIntegrationConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/index.d.ts CHANGED
@@ -533,6 +533,18 @@ type BitbucketCloudIntegrationConfig = {
533
533
  * See https://support.atlassian.com/bitbucket-cloud/docs/api-tokens/
534
534
  */
535
535
  token?: string;
536
+ /**
537
+ * The OAuth client ID for Bitbucket Cloud.
538
+ *
539
+ * See https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/
540
+ */
541
+ clientId?: string;
542
+ /**
543
+ * The OAuth client secret for Bitbucket Cloud.
544
+ *
545
+ * See https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/
546
+ */
547
+ clientSecret?: string;
536
548
  /** PGP private key for signing commits. */
537
549
  commitSigningKey?: string;
538
550
  };
@@ -900,6 +912,10 @@ type GithubAppConfig = {
900
912
  * https://docs.github.com/en/rest/reference/apps#list-installations-for-the-authenticated-app--code-samples
901
913
  */
902
914
  allowedInstallationOwners?: string[];
915
+ /**
916
+ * If true, then an installation token will be issued for access when no other token is available.
917
+ */
918
+ publicAccess?: boolean;
903
919
  };
904
920
  /**
905
921
  * Reads a single GitHub integration config.
@@ -1345,6 +1361,16 @@ declare function getBitbucketRequestOptions(config: BitbucketIntegrationConfig):
1345
1361
  headers: Record<string, string>;
1346
1362
  };
1347
1363
 
1364
+ /**
1365
+ * Fetches an OAuth access token from Bitbucket Cloud using client credentials flow.
1366
+ * Tokens are cached with a 10-minute grace period to account for clock skew.
1367
+ * Implements concurrent refresh protection to prevent multiple simultaneous token requests.
1368
+ *
1369
+ * @param clientId - OAuth client ID
1370
+ * @param clientSecret - OAuth client secret
1371
+ * @public
1372
+ */
1373
+ declare function getBitbucketCloudOAuthToken(clientId: string, clientSecret: string): Promise<string>;
1348
1374
  /**
1349
1375
  * Given a URL pointing to a path on a provider, returns the default branch.
1350
1376
  *
@@ -1380,14 +1406,14 @@ declare function getBitbucketCloudFileFetchUrl(url: string, config: BitbucketClo
1380
1406
  /**
1381
1407
  * Gets the request options necessary to make requests to a given provider.
1382
1408
  * Returns headers for authenticating with Bitbucket Cloud.
1383
- * Supports both username/token and username/appPassword auth.
1409
+ * Supports OAuth (clientId/clientSecret), username/token, and username/appPassword auth.
1384
1410
  *
1385
1411
  * @param config - The relevant provider config
1386
1412
  * @public
1387
1413
  */
1388
- declare function getBitbucketCloudRequestOptions(config: BitbucketCloudIntegrationConfig): {
1414
+ declare function getBitbucketCloudRequestOptions(config: BitbucketCloudIntegrationConfig): Promise<{
1389
1415
  headers: Record<string, string>;
1390
- };
1416
+ }>;
1391
1417
 
1392
1418
  /**
1393
1419
  * Given a URL pointing to a path on a provider, returns the default branch.
@@ -2031,5 +2057,5 @@ declare class ScmIntegrations implements ScmIntegrationRegistry {
2031
2057
  resolveEditUrl(url: string): string;
2032
2058
  }
2033
2059
 
2034
- export { AwsCodeCommitIntegration, AwsS3Integration, AzureBlobStorageIntergation, AzureIntegration, BitbucketCloudIntegration, BitbucketIntegration, BitbucketServerIntegration, DefaultAzureCredentialsManager, DefaultAzureDevOpsCredentialsProvider, DefaultGithubCredentialsProvider, DefaultGitlabCredentialsProvider, GerritIntegration, GitLabIntegration, GiteaIntegration, GithubAppCredentialsMux, GithubIntegration, HarnessIntegration, ScmIntegrations, SingleInstanceGithubCredentialsProvider, buildGerritGitilesArchiveUrl, buildGerritGitilesArchiveUrlFromLocation, defaultScmResolveUrl, getAzureCommitsUrl, getAzureDownloadUrl, getAzureFileFetchUrl, getAzureRequestOptions, getBitbucketCloudDefaultBranch, getBitbucketCloudDownloadUrl, getBitbucketCloudFileFetchUrl, getBitbucketCloudRequestOptions, getBitbucketDefaultBranch, getBitbucketDownloadUrl, getBitbucketFileFetchUrl, getBitbucketRequestOptions, getBitbucketServerDefaultBranch, getBitbucketServerDownloadUrl, getBitbucketServerFileFetchUrl, getBitbucketServerRequestOptions, getGerritBranchApiUrl, getGerritCloneRepoUrl, getGerritFileContentsApiUrl, getGerritProjectsApiUrl, getGerritRequestOptions, getGitHubRequestOptions, getGitLabFileFetchUrl, getGitLabIntegrationRelativePath, getGitLabRequestOptions, getGiteaArchiveUrl, getGiteaEditContentsUrl, getGiteaFileContentsUrl, getGiteaLatestCommitUrl, getGiteaRequestOptions, getGithubFileFetchUrl, getGitilesAuthenticationUrl, getHarnessArchiveUrl, getHarnessFileContentsUrl, getHarnessLatestCommitUrl, getHarnessRequestOptions, parseGerritGitilesUrl, parseGerritJsonResponse, parseGiteaUrl, parseGitilesUrlRef, parseHarnessUrl, readAwsCodeCommitIntegrationConfig, readAwsCodeCommitIntegrationConfigs, readAwsS3IntegrationConfig, readAwsS3IntegrationConfigs, readAzureBlobStorageIntegrationConfig, readAzureBlobStorageIntegrationConfigs, readAzureIntegrationConfig, readAzureIntegrationConfigs, readBitbucketCloudIntegrationConfig, readBitbucketCloudIntegrationConfigs, readBitbucketIntegrationConfig, readBitbucketIntegrationConfigs, readBitbucketServerIntegrationConfig, readBitbucketServerIntegrationConfigs, readGerritIntegrationConfig, readGerritIntegrationConfigs, readGitLabIntegrationConfig, readGitLabIntegrationConfigs, readGiteaConfig, readGithubIntegrationConfig, readGithubIntegrationConfigs, readGoogleGcsIntegrationConfig, readHarnessConfig, replaceGitLabUrlType, replaceGithubUrlType };
2060
+ export { AwsCodeCommitIntegration, AwsS3Integration, AzureBlobStorageIntergation, AzureIntegration, BitbucketCloudIntegration, BitbucketIntegration, BitbucketServerIntegration, DefaultAzureCredentialsManager, DefaultAzureDevOpsCredentialsProvider, DefaultGithubCredentialsProvider, DefaultGitlabCredentialsProvider, GerritIntegration, GitLabIntegration, GiteaIntegration, GithubAppCredentialsMux, GithubIntegration, HarnessIntegration, ScmIntegrations, SingleInstanceGithubCredentialsProvider, buildGerritGitilesArchiveUrl, buildGerritGitilesArchiveUrlFromLocation, defaultScmResolveUrl, getAzureCommitsUrl, getAzureDownloadUrl, getAzureFileFetchUrl, getAzureRequestOptions, getBitbucketCloudDefaultBranch, getBitbucketCloudDownloadUrl, getBitbucketCloudFileFetchUrl, getBitbucketCloudOAuthToken, getBitbucketCloudRequestOptions, getBitbucketDefaultBranch, getBitbucketDownloadUrl, getBitbucketFileFetchUrl, getBitbucketRequestOptions, getBitbucketServerDefaultBranch, getBitbucketServerDownloadUrl, getBitbucketServerFileFetchUrl, getBitbucketServerRequestOptions, getGerritBranchApiUrl, getGerritCloneRepoUrl, getGerritFileContentsApiUrl, getGerritProjectsApiUrl, getGerritRequestOptions, getGitHubRequestOptions, getGitLabFileFetchUrl, getGitLabIntegrationRelativePath, getGitLabRequestOptions, getGiteaArchiveUrl, getGiteaEditContentsUrl, getGiteaFileContentsUrl, getGiteaLatestCommitUrl, getGiteaRequestOptions, getGithubFileFetchUrl, getGitilesAuthenticationUrl, getHarnessArchiveUrl, getHarnessFileContentsUrl, getHarnessLatestCommitUrl, getHarnessRequestOptions, parseGerritGitilesUrl, parseGerritJsonResponse, parseGiteaUrl, parseGitilesUrlRef, parseHarnessUrl, readAwsCodeCommitIntegrationConfig, readAwsCodeCommitIntegrationConfigs, readAwsS3IntegrationConfig, readAwsS3IntegrationConfigs, readAzureBlobStorageIntegrationConfig, readAzureBlobStorageIntegrationConfigs, readAzureIntegrationConfig, readAzureIntegrationConfigs, readBitbucketCloudIntegrationConfig, readBitbucketCloudIntegrationConfigs, readBitbucketIntegrationConfig, readBitbucketIntegrationConfigs, readBitbucketServerIntegrationConfig, readBitbucketServerIntegrationConfigs, readGerritIntegrationConfig, readGerritIntegrationConfigs, readGitLabIntegrationConfig, readGitLabIntegrationConfigs, readGiteaConfig, readGithubIntegrationConfig, readGithubIntegrationConfigs, readGoogleGcsIntegrationConfig, readHarnessConfig, replaceGitLabUrlType, replaceGithubUrlType };
2035
2061
  export type { AwsCodeCommitIntegrationConfig, AwsS3IntegrationConfig, AzureBlobStorageIntegrationConfig, AzureClientSecretCredential, AzureCredentialBase, AzureCredentialsManager, AzureDevOpsCredential, AzureDevOpsCredentialKind, AzureDevOpsCredentialLike, AzureDevOpsCredentialType, AzureDevOpsCredentials, AzureDevOpsCredentialsProvider, AzureIntegrationConfig, AzureManagedIdentityClientAssertionCredential, AzureManagedIdentityCredential, BitbucketCloudIntegrationConfig, BitbucketIntegrationConfig, BitbucketServerIntegrationConfig, GerritIntegrationConfig, GitLabIntegrationConfig, GiteaIntegrationConfig, GithubAppConfig, GithubCredentialType, GithubCredentials, GithubCredentialsProvider, GithubIntegrationConfig, GitlabCredentials, GitlabCredentialsProvider, GoogleGcsIntegrationConfig, HarnessIntegrationConfig, IntegrationsByType, PersonalAccessTokenCredential, RateLimitInfo, ScmIntegration, ScmIntegrationRegistry, ScmIntegrationsFactory, ScmIntegrationsGroup };
package/dist/index.esm.js CHANGED
@@ -15,7 +15,7 @@ export { readBitbucketIntegrationConfig, readBitbucketIntegrationConfigs } from
15
15
  export { getBitbucketDefaultBranch, getBitbucketDownloadUrl, getBitbucketFileFetchUrl, getBitbucketRequestOptions } from './bitbucket/core.esm.js';
16
16
  export { BitbucketCloudIntegration } from './bitbucketCloud/BitbucketCloudIntegration.esm.js';
17
17
  export { readBitbucketCloudIntegrationConfig, readBitbucketCloudIntegrationConfigs } from './bitbucketCloud/config.esm.js';
18
- export { getBitbucketCloudDefaultBranch, getBitbucketCloudDownloadUrl, getBitbucketCloudFileFetchUrl, getBitbucketCloudRequestOptions } from './bitbucketCloud/core.esm.js';
18
+ export { getBitbucketCloudDefaultBranch, getBitbucketCloudDownloadUrl, getBitbucketCloudFileFetchUrl, getBitbucketCloudOAuthToken, getBitbucketCloudRequestOptions } from './bitbucketCloud/core.esm.js';
19
19
  export { BitbucketServerIntegration } from './bitbucketServer/BitbucketServerIntegration.esm.js';
20
20
  export { readBitbucketServerIntegrationConfig, readBitbucketServerIntegrationConfigs } from './bitbucketServer/config.esm.js';
21
21
  export { getBitbucketServerDefaultBranch, getBitbucketServerDownloadUrl, getBitbucketServerFileFetchUrl, getBitbucketServerRequestOptions } from './bitbucketServer/core.esm.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/integration",
3
- "version": "1.18.3-next.1",
3
+ "version": "1.19.1",
4
4
  "description": "Helpers for managing integrations towards external systems",
5
5
  "backstage": {
6
6
  "role": "common-library"
@@ -39,8 +39,8 @@
39
39
  "dependencies": {
40
40
  "@azure/identity": "^4.0.0",
41
41
  "@azure/storage-blob": "^12.5.0",
42
- "@backstage/config": "1.3.6",
43
- "@backstage/errors": "1.2.7",
42
+ "@backstage/config": "^1.3.6",
43
+ "@backstage/errors": "^1.2.7",
44
44
  "@octokit/auth-app": "^4.0.0",
45
45
  "@octokit/rest": "^19.0.3",
46
46
  "cross-fetch": "^4.0.0",
@@ -49,8 +49,8 @@
49
49
  "luxon": "^3.0.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@backstage/cli": "0.35.0-next.2",
53
- "@backstage/config-loader": "1.10.7-next.1",
52
+ "@backstage/cli": "^0.35.1",
53
+ "@backstage/config-loader": "^1.10.7",
54
54
  "msw": "^1.0.0"
55
55
  },
56
56
  "configSchema": "config.d.ts",