@backstage/core-app-api 1.20.2 → 1.20.3

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,23 @@
1
1
  # @backstage/core-app-api
2
2
 
3
+ ## 1.20.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 3ef5974: Added support for wildcard plugin entries in frontend discovery endpoints, matching the backend discovery behavior.
8
+ - 6172725: Fixed the default fetch API to support discovery endpoints that only define an internal target.
9
+ - Updated dependencies
10
+ - @backstage/ui@0.17.0
11
+ - @backstage/core-plugin-api@1.12.8
12
+
13
+ ## 1.20.3-next.0
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies
18
+ - @backstage/ui@0.17.0-next.0
19
+ - @backstage/core-plugin-api@1.12.8-next.0
20
+
3
21
  ## 1.20.2
4
22
 
5
23
  ### Patch Changes
@@ -0,0 +1,223 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "properties": {
5
+ "app": {
6
+ "type": "object",
7
+ "properties": {
8
+ "baseUrl": {
9
+ "type": "string",
10
+ "description": "The public absolute root URL that the frontend.",
11
+ "visibility": "frontend"
12
+ },
13
+ "title": {
14
+ "type": "string",
15
+ "description": "The title of the app, as shown in the Backstage web interface.",
16
+ "visibility": "frontend"
17
+ },
18
+ "support": {
19
+ "type": "object",
20
+ "properties": {
21
+ "url": {
22
+ "type": "string",
23
+ "description": "The primary support url.",
24
+ "visibility": "frontend"
25
+ },
26
+ "items": {
27
+ "type": "array",
28
+ "items": {
29
+ "type": "object",
30
+ "properties": {
31
+ "title": {
32
+ "type": "string",
33
+ "description": "The title of the support item grouping.",
34
+ "visibility": "frontend"
35
+ },
36
+ "icon": {
37
+ "type": "string",
38
+ "description": "An optional icon for the support item grouping.",
39
+ "visibility": "frontend"
40
+ },
41
+ "links": {
42
+ "type": "array",
43
+ "items": {
44
+ "type": "object",
45
+ "properties": {
46
+ "url": {
47
+ "type": "string",
48
+ "visibility": "frontend"
49
+ },
50
+ "title": {
51
+ "type": "string",
52
+ "visibility": "frontend"
53
+ }
54
+ },
55
+ "required": [
56
+ "url"
57
+ ]
58
+ },
59
+ "description": "A list of support links for the Backstage instance inside this grouping."
60
+ }
61
+ },
62
+ "required": [
63
+ "title",
64
+ "links"
65
+ ]
66
+ },
67
+ "description": "A list of categorized support item groupings."
68
+ }
69
+ },
70
+ "required": [
71
+ "url",
72
+ "items"
73
+ ],
74
+ "description": "Information about support of this Backstage instance and how to contact the integrator team."
75
+ },
76
+ "routes": {
77
+ "type": "object",
78
+ "properties": {
79
+ "bindings": {
80
+ "type": "object",
81
+ "additionalProperties": {
82
+ "anyOf": [
83
+ {
84
+ "type": "string"
85
+ },
86
+ {
87
+ "type": "boolean",
88
+ "const": false
89
+ }
90
+ ]
91
+ },
92
+ "description": "Maps external route references to regular route references. Both the key and the value is expected to be on the form `<pluginId>.<routeId>`. If the value is `false`, the route will be disabled even if it has a default mapping.",
93
+ "deepVisibility": "frontend"
94
+ }
95
+ }
96
+ }
97
+ },
98
+ "required": [
99
+ "baseUrl"
100
+ ],
101
+ "description": "Generic frontend configuration."
102
+ },
103
+ "backend": {
104
+ "type": "object",
105
+ "properties": {
106
+ "baseUrl": {
107
+ "type": "string",
108
+ "description": "The public absolute root URL that the backend is reachable at, from the browser's perspective.",
109
+ "visibility": "frontend"
110
+ }
111
+ },
112
+ "required": [
113
+ "baseUrl"
114
+ ],
115
+ "description": "Generic backend configuration."
116
+ },
117
+ "organization": {
118
+ "type": "object",
119
+ "properties": {
120
+ "name": {
121
+ "type": "string",
122
+ "description": "The name of the organization that the app belongs to.",
123
+ "visibility": "frontend"
124
+ }
125
+ },
126
+ "description": "Configuration that provides information about the organization that the app is for."
127
+ },
128
+ "homepage": {
129
+ "type": "object",
130
+ "properties": {
131
+ "clocks": {
132
+ "type": "array",
133
+ "items": {
134
+ "type": "object",
135
+ "properties": {
136
+ "label": {
137
+ "type": "string",
138
+ "visibility": "frontend"
139
+ },
140
+ "timezone": {
141
+ "type": "string",
142
+ "visibility": "frontend"
143
+ }
144
+ },
145
+ "required": [
146
+ "label",
147
+ "timezone"
148
+ ]
149
+ }
150
+ }
151
+ },
152
+ "description": "This config was used by the HomepageTimer but has been replaced by the HeaderWorldClock in the home plugin",
153
+ "deprecated": "in favor of the HeaderWorldClock which is found in the home plugin"
154
+ },
155
+ "auth": {
156
+ "type": "object",
157
+ "properties": {
158
+ "environment": {
159
+ "type": "string",
160
+ "description": "The 'environment' attribute added as an optional parameter to have configurable environment value for `auth.providers`. default value: 'development' optional values: 'development' | 'production'",
161
+ "visibility": "frontend"
162
+ }
163
+ },
164
+ "description": "Configuration that provides information on available configured authentication providers."
165
+ },
166
+ "enableExperimentalRedirectFlow": {
167
+ "type": "boolean",
168
+ "description": "Enable redirect authentication flow type, instead of a popup for authentication.",
169
+ "visibility": "frontend"
170
+ },
171
+ "discovery": {
172
+ "type": "object",
173
+ "properties": {
174
+ "endpoints": {
175
+ "type": "array",
176
+ "items": {
177
+ "type": "object",
178
+ "properties": {
179
+ "target": {
180
+ "anyOf": [
181
+ {
182
+ "type": "string"
183
+ },
184
+ {
185
+ "type": "object",
186
+ "properties": {
187
+ "external": {
188
+ "type": "string",
189
+ "visibility": "frontend"
190
+ }
191
+ }
192
+ }
193
+ ],
194
+ "description": "The target baseUrl to use for the plugin\n\nCan be either a string or an object with internal and external keys. (Internal is used for the backend, external for the frontend) Targets with `{{pluginId}}` or `{{ pluginId }} in the url will be replaced with the pluginId.",
195
+ "visibility": "frontend"
196
+ },
197
+ "plugins": {
198
+ "type": "array",
199
+ "items": {
200
+ "type": "string"
201
+ },
202
+ "description": "Array of plugins which use the target baseUrl. Use `'*'` to configure a fallback target for all plugins without an explicit target.",
203
+ "visibility": "frontend"
204
+ }
205
+ },
206
+ "required": [
207
+ "target",
208
+ "plugins"
209
+ ]
210
+ },
211
+ "description": "Endpoints\n\nA list of target baseUrls and the associated plugins.",
212
+ "visibility": "frontend"
213
+ }
214
+ },
215
+ "description": "Discovery options.",
216
+ "visibility": "frontend"
217
+ }
218
+ },
219
+ "required": [
220
+ "app",
221
+ "backend"
222
+ ]
223
+ }
@@ -17,10 +17,14 @@ class FrontendHostDiscovery {
17
17
  * internal: https://internal.example.com/search
18
18
  * external: https://example.com/search
19
19
  * plugins: [search]
20
+ * - target: https://example.com/api/{{pluginId}}
21
+ * plugins: ['*']
20
22
  * ```
21
23
  *
22
- * If a plugin is not declared in the config, the discovery will fall back to using the baseUrl with
23
- * the provided `pathPattern` appended. The default path pattern is `"/api/{{ pluginId }}"`.
24
+ * If a plugin is not declared in the config, discovery will first use the
25
+ * wildcard target, if one is configured, and otherwise fall back to using
26
+ * the baseUrl with the provided `pathPattern` appended. The default path
27
+ * pattern is `"/api/{{ pluginId }}"`.
24
28
  */
25
29
  static fromConfig(config, options) {
26
30
  const path = options?.pathPattern ?? "/api/{{ pluginId }}";
@@ -45,7 +49,7 @@ class FrontendHostDiscovery {
45
49
  this.defaultEndpoint = defaultEndpoint;
46
50
  }
47
51
  async getBaseUrl(pluginId) {
48
- const endpoint = this.endpoints.get(pluginId);
52
+ const endpoint = this.endpoints.get(pluginId) ?? this.endpoints.get("*");
49
53
  if (endpoint) {
50
54
  return endpoint.getBaseUrl(pluginId);
51
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"FrontendHostDiscovery.esm.js","sources":["../../../../src/apis/implementations/DiscoveryApi/FrontendHostDiscovery.ts"],"sourcesContent":["/*\n * Copyright 2023 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 */\nimport { Config } from '@backstage/config';\nimport { DiscoveryApi } from '@backstage/core-plugin-api';\nimport { UrlPatternDiscovery } from './UrlPatternDiscovery';\n\n/**\n * FrontendHostDiscovery is a config driven DiscoveryApi implementation.\n * It uses the app-config to determine the url for a plugin.\n *\n * @public\n */\nexport class FrontendHostDiscovery implements DiscoveryApi {\n /**\n * Creates a new FrontendHostDiscovery discovery instance by reading\n * the external target URL from the `discovery.endpoints` config section.\n *\n * eg.\n * ```yaml\n * discovery:\n * endpoints:\n * - target: https://internal.example.com/internal-catalog\n * plugins: [catalog]\n * - target: https://internal.example.com/secure/api/{{pluginId}}\n * plugins: [auth, permissions]\n * - target:\n * internal: https://internal.example.com/search\n * external: https://example.com/search\n * plugins: [search]\n * ```\n *\n * If a plugin is not declared in the config, the discovery will fall back to using the baseUrl with\n * the provided `pathPattern` appended. The default path pattern is `\"/api/{{ pluginId }}\"`.\n */\n static fromConfig(config: Config, options?: { pathPattern?: string }) {\n const path = options?.pathPattern ?? '/api/{{ pluginId }}';\n const baseUrl = config.getString('backend.baseUrl');\n\n const endpoints = config\n .getOptionalConfigArray('discovery.endpoints')\n ?.flatMap(e => {\n const target =\n typeof e.get('target') === 'object'\n ? e.getOptionalString('target.external')\n : e.getString('target');\n if (!target) {\n return [];\n }\n const discovery = UrlPatternDiscovery.compile(target);\n return e\n .getStringArray('plugins')\n .map(pluginId => [pluginId, discovery] as const);\n });\n\n return new FrontendHostDiscovery(\n new Map(endpoints),\n UrlPatternDiscovery.compile(`${baseUrl}${path}`),\n );\n }\n\n private readonly endpoints: Map<string, DiscoveryApi>;\n private readonly defaultEndpoint: DiscoveryApi;\n\n private constructor(\n endpoints: Map<string, DiscoveryApi>,\n defaultEndpoint: DiscoveryApi,\n ) {\n this.endpoints = endpoints;\n this.defaultEndpoint = defaultEndpoint;\n }\n\n async getBaseUrl(pluginId: string): Promise<string> {\n const endpoint = this.endpoints.get(pluginId);\n if (endpoint) {\n return endpoint.getBaseUrl(pluginId);\n }\n return this.defaultEndpoint.getBaseUrl(pluginId);\n }\n}\n"],"names":[],"mappings":";;AAyBO,MAAM,qBAAA,CAA8C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBzD,OAAO,UAAA,CAAW,MAAA,EAAgB,OAAA,EAAoC;AACpE,IAAA,MAAM,IAAA,GAAO,SAAS,WAAA,IAAe,qBAAA;AACrC,IAAA,MAAM,OAAA,GAAU,MAAA,CAAO,SAAA,CAAU,iBAAiB,CAAA;AAElD,IAAA,MAAM,YAAY,MAAA,CACf,sBAAA,CAAuB,qBAAqB,CAAA,EAC3C,QAAQ,CAAA,CAAA,KAAK;AACb,MAAA,MAAM,MAAA,GACJ,OAAO,CAAA,CAAE,GAAA,CAAI,QAAQ,CAAA,KAAM,QAAA,GACvB,CAAA,CAAE,iBAAA,CAAkB,iBAAiB,CAAA,GACrC,CAAA,CAAE,UAAU,QAAQ,CAAA;AAC1B,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,OAAO,EAAC;AAAA,MACV;AACA,MAAA,MAAM,SAAA,GAAY,mBAAA,CAAoB,OAAA,CAAQ,MAAM,CAAA;AACpD,MAAA,OAAO,CAAA,CACJ,eAAe,SAAS,CAAA,CACxB,IAAI,CAAA,QAAA,KAAY,CAAC,QAAA,EAAU,SAAS,CAAU,CAAA;AAAA,IACnD,CAAC,CAAA;AAEH,IAAA,OAAO,IAAI,qBAAA;AAAA,MACT,IAAI,IAAI,SAAS,CAAA;AAAA,MACjB,oBAAoB,OAAA,CAAQ,CAAA,EAAG,OAAO,CAAA,EAAG,IAAI,CAAA,CAAE;AAAA,KACjD;AAAA,EACF;AAAA,EAEiB,SAAA;AAAA,EACA,eAAA;AAAA,EAET,WAAA,CACN,WACA,eAAA,EACA;AACA,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AACjB,IAAA,IAAA,CAAK,eAAA,GAAkB,eAAA;AAAA,EACzB;AAAA,EAEA,MAAM,WAAW,QAAA,EAAmC;AAClD,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA;AAC5C,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,OAAO,QAAA,CAAS,WAAW,QAAQ,CAAA;AAAA,IACrC;AACA,IAAA,OAAO,IAAA,CAAK,eAAA,CAAgB,UAAA,CAAW,QAAQ,CAAA;AAAA,EACjD;AACF;;;;"}
1
+ {"version":3,"file":"FrontendHostDiscovery.esm.js","sources":["../../../../src/apis/implementations/DiscoveryApi/FrontendHostDiscovery.ts"],"sourcesContent":["/*\n * Copyright 2023 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 */\nimport { Config } from '@backstage/config';\nimport { DiscoveryApi } from '@backstage/core-plugin-api';\nimport { UrlPatternDiscovery } from './UrlPatternDiscovery';\n\n/**\n * FrontendHostDiscovery is a config driven DiscoveryApi implementation.\n * It uses the app-config to determine the url for a plugin.\n *\n * @public\n */\nexport class FrontendHostDiscovery implements DiscoveryApi {\n /**\n * Creates a new FrontendHostDiscovery discovery instance by reading\n * the external target URL from the `discovery.endpoints` config section.\n *\n * eg.\n * ```yaml\n * discovery:\n * endpoints:\n * - target: https://internal.example.com/internal-catalog\n * plugins: [catalog]\n * - target: https://internal.example.com/secure/api/{{pluginId}}\n * plugins: [auth, permissions]\n * - target:\n * internal: https://internal.example.com/search\n * external: https://example.com/search\n * plugins: [search]\n * - target: https://example.com/api/{{pluginId}}\n * plugins: ['*']\n * ```\n *\n * If a plugin is not declared in the config, discovery will first use the\n * wildcard target, if one is configured, and otherwise fall back to using\n * the baseUrl with the provided `pathPattern` appended. The default path\n * pattern is `\"/api/{{ pluginId }}\"`.\n */\n static fromConfig(config: Config, options?: { pathPattern?: string }) {\n const path = options?.pathPattern ?? '/api/{{ pluginId }}';\n const baseUrl = config.getString('backend.baseUrl');\n\n const endpoints = config\n .getOptionalConfigArray('discovery.endpoints')\n ?.flatMap(e => {\n const target =\n typeof e.get('target') === 'object'\n ? e.getOptionalString('target.external')\n : e.getString('target');\n if (!target) {\n return [];\n }\n const discovery = UrlPatternDiscovery.compile(target);\n return e\n .getStringArray('plugins')\n .map(pluginId => [pluginId, discovery] as const);\n });\n\n return new FrontendHostDiscovery(\n new Map(endpoints),\n UrlPatternDiscovery.compile(`${baseUrl}${path}`),\n );\n }\n\n private readonly endpoints: Map<string, DiscoveryApi>;\n private readonly defaultEndpoint: DiscoveryApi;\n\n private constructor(\n endpoints: Map<string, DiscoveryApi>,\n defaultEndpoint: DiscoveryApi,\n ) {\n this.endpoints = endpoints;\n this.defaultEndpoint = defaultEndpoint;\n }\n\n async getBaseUrl(pluginId: string): Promise<string> {\n const endpoint = this.endpoints.get(pluginId) ?? this.endpoints.get('*');\n if (endpoint) {\n return endpoint.getBaseUrl(pluginId);\n }\n return this.defaultEndpoint.getBaseUrl(pluginId);\n }\n}\n"],"names":[],"mappings":";;AAyBO,MAAM,qBAAA,CAA8C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA0BzD,OAAO,UAAA,CAAW,MAAA,EAAgB,OAAA,EAAoC;AACpE,IAAA,MAAM,IAAA,GAAO,SAAS,WAAA,IAAe,qBAAA;AACrC,IAAA,MAAM,OAAA,GAAU,MAAA,CAAO,SAAA,CAAU,iBAAiB,CAAA;AAElD,IAAA,MAAM,YAAY,MAAA,CACf,sBAAA,CAAuB,qBAAqB,CAAA,EAC3C,QAAQ,CAAA,CAAA,KAAK;AACb,MAAA,MAAM,MAAA,GACJ,OAAO,CAAA,CAAE,GAAA,CAAI,QAAQ,CAAA,KAAM,QAAA,GACvB,CAAA,CAAE,iBAAA,CAAkB,iBAAiB,CAAA,GACrC,CAAA,CAAE,UAAU,QAAQ,CAAA;AAC1B,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,OAAO,EAAC;AAAA,MACV;AACA,MAAA,MAAM,SAAA,GAAY,mBAAA,CAAoB,OAAA,CAAQ,MAAM,CAAA;AACpD,MAAA,OAAO,CAAA,CACJ,eAAe,SAAS,CAAA,CACxB,IAAI,CAAA,QAAA,KAAY,CAAC,QAAA,EAAU,SAAS,CAAU,CAAA;AAAA,IACnD,CAAC,CAAA;AAEH,IAAA,OAAO,IAAI,qBAAA;AAAA,MACT,IAAI,IAAI,SAAS,CAAA;AAAA,MACjB,oBAAoB,OAAA,CAAQ,CAAA,EAAG,OAAO,CAAA,EAAG,IAAI,CAAA,CAAE;AAAA,KACjD;AAAA,EACF;AAAA,EAEiB,SAAA;AAAA,EACA,eAAA;AAAA,EAET,WAAA,CACN,WACA,eAAA,EACA;AACA,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AACjB,IAAA,IAAA,CAAK,eAAA,GAAkB,eAAA;AAAA,EACzB;AAAA,EAEA,MAAM,WAAW,QAAA,EAAmC;AAClD,IAAA,MAAM,QAAA,GAAW,KAAK,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA,IAAK,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,GAAG,CAAA;AACvE,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,OAAO,QAAA,CAAS,WAAW,QAAQ,CAAA;AAAA,IACrC;AACA,IAAA,OAAO,IAAA,CAAK,eAAA,CAAgB,UAAA,CAAW,QAAQ,CAAA;AAAA,EACjD;AACF;;;;"}
@@ -1,3 +1,4 @@
1
+ const PLUGIN_ID_TEMPLATE = /\{\{\s*pluginId\s*\}\}/g;
1
2
  class IdentityAuthInjectorFetchMiddleware {
2
3
  static create(options) {
3
4
  const matcher = buildMatcher(options);
@@ -17,11 +18,17 @@ class IdentityAuthInjectorFetchMiddleware {
17
18
  static getDiscoveryUrlPrefixes(config) {
18
19
  const endpointConfigs = config.getOptionalConfigArray("discovery.endpoints") || [];
19
20
  return endpointConfigs.flatMap((c) => {
20
- const target = typeof c.get("target") === "object" ? c.getString("target.external") : c.getString("target");
21
+ const target = getExternalTarget(c);
22
+ if (!target) {
23
+ return [];
24
+ }
21
25
  const plugins = c.getStringArray("plugins");
22
- return plugins.map(
23
- (pluginId) => target.replace(/\{\{\s*pluginId\s*\}\}/g, pluginId)
24
- );
26
+ return plugins.flatMap((pluginId) => {
27
+ if (pluginId === "*") {
28
+ return [];
29
+ }
30
+ return target.replace(PLUGIN_ID_TEMPLATE, pluginId);
31
+ });
25
32
  });
26
33
  }
27
34
  identityApi;
@@ -52,15 +59,43 @@ function buildMatcher(options) {
52
59
  } else if (options.urlPrefixAllowlist) {
53
60
  return buildPrefixMatcher(options.urlPrefixAllowlist);
54
61
  } else if (options.config) {
55
- return buildPrefixMatcher([
62
+ const prefixMatcher = buildPrefixMatcher([
56
63
  options.config.getString("backend.baseUrl"),
57
64
  ...IdentityAuthInjectorFetchMiddleware.getDiscoveryUrlPrefixes(
58
65
  options.config
59
66
  )
60
67
  ]);
68
+ const wildcardMatcher = buildWildcardDiscoveryMatcher(options.config);
69
+ return (url) => prefixMatcher(url) || wildcardMatcher(url);
61
70
  }
62
71
  return () => false;
63
72
  }
73
+ function getExternalTarget(config) {
74
+ return typeof config.get("target") === "object" ? config.getOptionalString("target.external") : config.getString("target");
75
+ }
76
+ function buildWildcardDiscoveryMatcher(config) {
77
+ const matchers = config.getOptionalConfigArray("discovery.endpoints")?.flatMap((endpoint) => {
78
+ if (!endpoint.getStringArray("plugins").includes("*")) {
79
+ return [];
80
+ }
81
+ const target = getExternalTarget(endpoint);
82
+ return target ? [buildUrlPatternMatcher(target)] : [];
83
+ }) ?? [];
84
+ return (url) => matchers.some((matcher) => matcher(url));
85
+ }
86
+ function buildUrlPatternMatcher(pattern) {
87
+ const parts = pattern.replace(/\/$/, "").split(PLUGIN_ID_TEMPLATE);
88
+ let source = escapeRegExp(parts[0]);
89
+ for (let i = 1; i < parts.length; i++) {
90
+ source += i === 1 ? "([^/?#]+)" : "\\1";
91
+ source += escapeRegExp(parts[i]);
92
+ }
93
+ const regex = new RegExp(`^${source}(?:/|$|[?#])`);
94
+ return (url) => regex.test(url);
95
+ }
96
+ function escapeRegExp(value) {
97
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
98
+ }
64
99
  function buildPrefixMatcher(prefixes) {
65
100
  const trimmedPrefixes = prefixes.map((prefix) => prefix.replace(/\/$/, ""));
66
101
  return (url) => trimmedPrefixes.some(
@@ -1 +1 @@
1
- {"version":3,"file":"IdentityAuthInjectorFetchMiddleware.esm.js","sources":["../../../../src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.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 { Config } from '@backstage/config';\nimport { IdentityApi } from '@backstage/core-plugin-api';\nimport { FetchMiddleware } from './types';\n\n/**\n * A fetch middleware, which injects a Backstage token header when the user is\n * signed in.\n */\nexport class IdentityAuthInjectorFetchMiddleware implements FetchMiddleware {\n static create(options: {\n identityApi: IdentityApi;\n config?: Config;\n urlPrefixAllowlist?: string[];\n allowUrl?: (url: string) => boolean;\n header?: {\n name: string;\n value: (backstageToken: string) => string;\n };\n }): IdentityAuthInjectorFetchMiddleware {\n const matcher = buildMatcher(options);\n const headerName = options.header?.name || 'authorization';\n const headerValue = options.header?.value || (token => `Bearer ${token}`);\n\n return new IdentityAuthInjectorFetchMiddleware(\n options.identityApi,\n matcher,\n headerName,\n headerValue,\n );\n }\n\n /**\n * Returns an array of plugin URL prefixes derived from the static `discovery`\n * configuration, to be used as `urlPrefixAllowlist` option of {@link create}.\n */\n static getDiscoveryUrlPrefixes(config: Config): string[] {\n const endpointConfigs =\n config.getOptionalConfigArray('discovery.endpoints') || [];\n return endpointConfigs.flatMap(c => {\n const target =\n typeof c.get('target') === 'object'\n ? c.getString('target.external')\n : c.getString('target');\n const plugins = c.getStringArray('plugins');\n return plugins.map(pluginId =>\n target.replace(/\\{\\{\\s*pluginId\\s*\\}\\}/g, pluginId),\n );\n });\n }\n\n public readonly identityApi: IdentityApi;\n public readonly allowUrl: (url: string) => boolean;\n public readonly headerName: string;\n public readonly headerValue: (pluginId: string) => string;\n\n constructor(\n identityApi: IdentityApi,\n allowUrl: (url: string) => boolean,\n headerName: string,\n headerValue: (pluginId: string) => string,\n ) {\n this.identityApi = identityApi;\n this.allowUrl = allowUrl;\n this.headerName = headerName;\n this.headerValue = headerValue;\n }\n\n apply(next: typeof fetch): typeof fetch {\n return async (input, init) => {\n // Skip this middleware if the header already exists, or if the URL\n // doesn't match any of the allowlist items, or if there was no token.\n // NOTE(freben): The \"as any\" casts here and below are because of subtle\n // undici type differences that happened in a node types bump. Those are\n // immaterial to the code at hand at runtime, as the global fetch and\n // Request are always taken from the same place.\n const request = new Request(input as any, init);\n const { token } = await this.identityApi.getCredentials();\n if (\n request.headers.get(this.headerName) ||\n typeof token !== 'string' ||\n !token ||\n !this.allowUrl(request.url)\n ) {\n return next(input as any, init);\n }\n\n request.headers.set(this.headerName, this.headerValue(token));\n return next(request);\n };\n }\n}\n\nfunction buildMatcher(options: {\n config?: Config;\n urlPrefixAllowlist?: string[];\n allowUrl?: (url: string) => boolean;\n}): (url: string) => boolean {\n if (options.allowUrl) {\n return options.allowUrl;\n } else if (options.urlPrefixAllowlist) {\n return buildPrefixMatcher(options.urlPrefixAllowlist);\n } else if (options.config) {\n return buildPrefixMatcher([\n options.config.getString('backend.baseUrl'),\n ...IdentityAuthInjectorFetchMiddleware.getDiscoveryUrlPrefixes(\n options.config,\n ),\n ]);\n }\n return () => false;\n}\n\nfunction buildPrefixMatcher(prefixes: string[]): (url: string) => boolean {\n const trimmedPrefixes = prefixes.map(prefix => prefix.replace(/\\/$/, ''));\n return url =>\n trimmedPrefixes.some(\n prefix => url === prefix || url.startsWith(`${prefix}/`),\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"IdentityAuthInjectorFetchMiddleware.esm.js","sources":["../../../../src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.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 { Config } from '@backstage/config';\nimport { IdentityApi } from '@backstage/core-plugin-api';\nimport { FetchMiddleware } from './types';\n\nconst PLUGIN_ID_TEMPLATE = /\\{\\{\\s*pluginId\\s*\\}\\}/g;\n\n/**\n * A fetch middleware, which injects a Backstage token header when the user is\n * signed in.\n */\nexport class IdentityAuthInjectorFetchMiddleware implements FetchMiddleware {\n static create(options: {\n identityApi: IdentityApi;\n config?: Config;\n urlPrefixAllowlist?: string[];\n allowUrl?: (url: string) => boolean;\n header?: {\n name: string;\n value: (backstageToken: string) => string;\n };\n }): IdentityAuthInjectorFetchMiddleware {\n const matcher = buildMatcher(options);\n const headerName = options.header?.name || 'authorization';\n const headerValue = options.header?.value || (token => `Bearer ${token}`);\n\n return new IdentityAuthInjectorFetchMiddleware(\n options.identityApi,\n matcher,\n headerName,\n headerValue,\n );\n }\n\n /**\n * Returns an array of plugin URL prefixes derived from the static `discovery`\n * configuration, to be used as `urlPrefixAllowlist` option of {@link create}.\n */\n static getDiscoveryUrlPrefixes(config: Config): string[] {\n const endpointConfigs =\n config.getOptionalConfigArray('discovery.endpoints') || [];\n return endpointConfigs.flatMap(c => {\n const target = getExternalTarget(c);\n if (!target) {\n return [];\n }\n const plugins = c.getStringArray('plugins');\n return plugins.flatMap(pluginId => {\n if (pluginId === '*') {\n return [];\n }\n return target.replace(PLUGIN_ID_TEMPLATE, pluginId);\n });\n });\n }\n\n public readonly identityApi: IdentityApi;\n public readonly allowUrl: (url: string) => boolean;\n public readonly headerName: string;\n public readonly headerValue: (pluginId: string) => string;\n\n constructor(\n identityApi: IdentityApi,\n allowUrl: (url: string) => boolean,\n headerName: string,\n headerValue: (pluginId: string) => string,\n ) {\n this.identityApi = identityApi;\n this.allowUrl = allowUrl;\n this.headerName = headerName;\n this.headerValue = headerValue;\n }\n\n apply(next: typeof fetch): typeof fetch {\n return async (input, init) => {\n // Skip this middleware if the header already exists, or if the URL\n // doesn't match any of the allowlist items, or if there was no token.\n // NOTE(freben): The \"as any\" casts here and below are because of subtle\n // undici type differences that happened in a node types bump. Those are\n // immaterial to the code at hand at runtime, as the global fetch and\n // Request are always taken from the same place.\n const request = new Request(input as any, init);\n const { token } = await this.identityApi.getCredentials();\n if (\n request.headers.get(this.headerName) ||\n typeof token !== 'string' ||\n !token ||\n !this.allowUrl(request.url)\n ) {\n return next(input as any, init);\n }\n\n request.headers.set(this.headerName, this.headerValue(token));\n return next(request);\n };\n }\n}\n\nfunction buildMatcher(options: {\n config?: Config;\n urlPrefixAllowlist?: string[];\n allowUrl?: (url: string) => boolean;\n}): (url: string) => boolean {\n if (options.allowUrl) {\n return options.allowUrl;\n } else if (options.urlPrefixAllowlist) {\n return buildPrefixMatcher(options.urlPrefixAllowlist);\n } else if (options.config) {\n const prefixMatcher = buildPrefixMatcher([\n options.config.getString('backend.baseUrl'),\n ...IdentityAuthInjectorFetchMiddleware.getDiscoveryUrlPrefixes(\n options.config,\n ),\n ]);\n const wildcardMatcher = buildWildcardDiscoveryMatcher(options.config);\n return url => prefixMatcher(url) || wildcardMatcher(url);\n }\n return () => false;\n}\n\nfunction getExternalTarget(config: Config): string | undefined {\n return typeof config.get('target') === 'object'\n ? config.getOptionalString('target.external')\n : config.getString('target');\n}\n\nfunction buildWildcardDiscoveryMatcher(\n config: Config,\n): (url: string) => boolean {\n const matchers =\n config.getOptionalConfigArray('discovery.endpoints')?.flatMap(endpoint => {\n if (!endpoint.getStringArray('plugins').includes('*')) {\n return [];\n }\n const target = getExternalTarget(endpoint);\n return target ? [buildUrlPatternMatcher(target)] : [];\n }) ?? [];\n\n return url => matchers.some(matcher => matcher(url));\n}\n\nfunction buildUrlPatternMatcher(pattern: string): (url: string) => boolean {\n const parts = pattern.replace(/\\/$/, '').split(PLUGIN_ID_TEMPLATE);\n let source = escapeRegExp(parts[0]);\n\n for (let i = 1; i < parts.length; i++) {\n source += i === 1 ? '([^/?#]+)' : '\\\\1';\n source += escapeRegExp(parts[i]);\n }\n\n const regex = new RegExp(`^${source}(?:/|$|[?#])`);\n return url => regex.test(url);\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction buildPrefixMatcher(prefixes: string[]): (url: string) => boolean {\n const trimmedPrefixes = prefixes.map(prefix => prefix.replace(/\\/$/, ''));\n return url =>\n trimmedPrefixes.some(\n prefix => url === prefix || url.startsWith(`${prefix}/`),\n );\n}\n"],"names":[],"mappings":"AAoBA,MAAM,kBAAA,GAAqB,yBAAA;AAMpB,MAAM,mCAAA,CAA+D;AAAA,EAC1E,OAAO,OAAO,OAAA,EAS0B;AACtC,IAAA,MAAM,OAAA,GAAU,aAAa,OAAO,CAAA;AACpC,IAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,MAAA,EAAQ,IAAA,IAAQ,eAAA;AAC3C,IAAA,MAAM,cAAc,OAAA,CAAQ,MAAA,EAAQ,KAAA,KAAU,CAAA,KAAA,KAAS,UAAU,KAAK,CAAA,CAAA,CAAA;AAEtE,IAAA,OAAO,IAAI,mCAAA;AAAA,MACT,OAAA,CAAQ,WAAA;AAAA,MACR,OAAA;AAAA,MACA,UAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,wBAAwB,MAAA,EAA0B;AACvD,IAAA,MAAM,eAAA,GACJ,MAAA,CAAO,sBAAA,CAAuB,qBAAqB,KAAK,EAAC;AAC3D,IAAA,OAAO,eAAA,CAAgB,QAAQ,CAAA,CAAA,KAAK;AAClC,MAAA,MAAM,MAAA,GAAS,kBAAkB,CAAC,CAAA;AAClC,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,OAAO,EAAC;AAAA,MACV;AACA,MAAA,MAAM,OAAA,GAAU,CAAA,CAAE,cAAA,CAAe,SAAS,CAAA;AAC1C,MAAA,OAAO,OAAA,CAAQ,QAAQ,CAAA,QAAA,KAAY;AACjC,QAAA,IAAI,aAAa,GAAA,EAAK;AACpB,UAAA,OAAO,EAAC;AAAA,QACV;AACA,QAAA,OAAO,MAAA,CAAO,OAAA,CAAQ,kBAAA,EAAoB,QAAQ,CAAA;AAAA,MACpD,CAAC,CAAA;AAAA,IACH,CAAC,CAAA;AAAA,EACH;AAAA,EAEgB,WAAA;AAAA,EACA,QAAA;AAAA,EACA,UAAA;AAAA,EACA,WAAA;AAAA,EAEhB,WAAA,CACE,WAAA,EACA,QAAA,EACA,UAAA,EACA,WAAA,EACA;AACA,IAAA,IAAA,CAAK,WAAA,GAAc,WAAA;AACnB,IAAA,IAAA,CAAK,QAAA,GAAW,QAAA;AAChB,IAAA,IAAA,CAAK,UAAA,GAAa,UAAA;AAClB,IAAA,IAAA,CAAK,WAAA,GAAc,WAAA;AAAA,EACrB;AAAA,EAEA,MAAM,IAAA,EAAkC;AACtC,IAAA,OAAO,OAAO,OAAO,IAAA,KAAS;AAO5B,MAAA,MAAM,OAAA,GAAU,IAAI,OAAA,CAAQ,KAAA,EAAc,IAAI,CAAA;AAC9C,MAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAM,IAAA,CAAK,YAAY,cAAA,EAAe;AACxD,MAAA,IACE,QAAQ,OAAA,CAAQ,GAAA,CAAI,IAAA,CAAK,UAAU,KACnC,OAAO,KAAA,KAAU,QAAA,IACjB,CAAC,SACD,CAAC,IAAA,CAAK,QAAA,CAAS,OAAA,CAAQ,GAAG,CAAA,EAC1B;AACA,QAAA,OAAO,IAAA,CAAK,OAAc,IAAI,CAAA;AAAA,MAChC;AAEA,MAAA,OAAA,CAAQ,QAAQ,GAAA,CAAI,IAAA,CAAK,YAAY,IAAA,CAAK,WAAA,CAAY,KAAK,CAAC,CAAA;AAC5D,MAAA,OAAO,KAAK,OAAO,CAAA;AAAA,IACrB,CAAA;AAAA,EACF;AACF;AAEA,SAAS,aAAa,OAAA,EAIO;AAC3B,EAAA,IAAI,QAAQ,QAAA,EAAU;AACpB,IAAA,OAAO,OAAA,CAAQ,QAAA;AAAA,EACjB,CAAA,MAAA,IAAW,QAAQ,kBAAA,EAAoB;AACrC,IAAA,OAAO,kBAAA,CAAmB,QAAQ,kBAAkB,CAAA;AAAA,EACtD,CAAA,MAAA,IAAW,QAAQ,MAAA,EAAQ;AACzB,IAAA,MAAM,gBAAgB,kBAAA,CAAmB;AAAA,MACvC,OAAA,CAAQ,MAAA,CAAO,SAAA,CAAU,iBAAiB,CAAA;AAAA,MAC1C,GAAG,mCAAA,CAAoC,uBAAA;AAAA,QACrC,OAAA,CAAQ;AAAA;AACV,KACD,CAAA;AACD,IAAA,MAAM,eAAA,GAAkB,6BAAA,CAA8B,OAAA,CAAQ,MAAM,CAAA;AACpE,IAAA,OAAO,CAAA,GAAA,KAAO,aAAA,CAAc,GAAG,CAAA,IAAK,gBAAgB,GAAG,CAAA;AAAA,EACzD;AACA,EAAA,OAAO,MAAM,KAAA;AACf;AAEA,SAAS,kBAAkB,MAAA,EAAoC;AAC7D,EAAA,OAAO,OAAO,MAAA,CAAO,GAAA,CAAI,QAAQ,CAAA,KAAM,QAAA,GACnC,MAAA,CAAO,iBAAA,CAAkB,iBAAiB,CAAA,GAC1C,MAAA,CAAO,SAAA,CAAU,QAAQ,CAAA;AAC/B;AAEA,SAAS,8BACP,MAAA,EAC0B;AAC1B,EAAA,MAAM,WACJ,MAAA,CAAO,sBAAA,CAAuB,qBAAqB,CAAA,EAAG,QAAQ,CAAA,QAAA,KAAY;AACxE,IAAA,IAAI,CAAC,QAAA,CAAS,cAAA,CAAe,SAAS,CAAA,CAAE,QAAA,CAAS,GAAG,CAAA,EAAG;AACrD,MAAA,OAAO,EAAC;AAAA,IACV;AACA,IAAA,MAAM,MAAA,GAAS,kBAAkB,QAAQ,CAAA;AACzC,IAAA,OAAO,SAAS,CAAC,sBAAA,CAAuB,MAAM,CAAC,IAAI,EAAC;AAAA,EACtD,CAAC,KAAK,EAAC;AAET,EAAA,OAAO,SAAO,QAAA,CAAS,IAAA,CAAK,CAAA,OAAA,KAAW,OAAA,CAAQ,GAAG,CAAC,CAAA;AACrD;AAEA,SAAS,uBAAuB,OAAA,EAA2C;AACzE,EAAA,MAAM,QAAQ,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAA,CAAE,MAAM,kBAAkB,CAAA;AACjE,EAAA,IAAI,MAAA,GAAS,YAAA,CAAa,KAAA,CAAM,CAAC,CAAC,CAAA;AAElC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,MAAA,IAAU,CAAA,KAAM,IAAI,WAAA,GAAc,KAAA;AAClC,IAAA,MAAA,IAAU,YAAA,CAAa,KAAA,CAAM,CAAC,CAAC,CAAA;AAAA,EACjC;AAEA,EAAA,MAAM,KAAA,GAAQ,IAAI,MAAA,CAAO,CAAA,CAAA,EAAI,MAAM,CAAA,YAAA,CAAc,CAAA;AACjD,EAAA,OAAO,CAAA,GAAA,KAAO,KAAA,CAAM,IAAA,CAAK,GAAG,CAAA;AAC9B;AAEA,SAAS,aAAa,KAAA,EAAuB;AAC3C,EAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,qBAAA,EAAuB,MAAM,CAAA;AACpD;AAEA,SAAS,mBAAmB,QAAA,EAA8C;AACxE,EAAA,MAAM,eAAA,GAAkB,SAAS,GAAA,CAAI,CAAA,MAAA,KAAU,OAAO,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAC,CAAA;AACxE,EAAA,OAAO,SACL,eAAA,CAAgB,IAAA;AAAA,IACd,YAAU,GAAA,KAAQ,MAAA,IAAU,IAAI,UAAA,CAAW,CAAA,EAAG,MAAM,CAAA,CAAA,CAAG;AAAA,GACzD;AACJ;;;;"}
package/dist/index.d.ts CHANGED
@@ -559,10 +559,14 @@ declare class FrontendHostDiscovery implements DiscoveryApi {
559
559
  * internal: https://internal.example.com/search
560
560
  * external: https://example.com/search
561
561
  * plugins: [search]
562
+ * - target: https://example.com/api/{{pluginId}}
563
+ * plugins: ['*']
562
564
  * ```
563
565
  *
564
- * If a plugin is not declared in the config, the discovery will fall back to using the baseUrl with
565
- * the provided `pathPattern` appended. The default path pattern is `"/api/{{ pluginId }}"`.
566
+ * If a plugin is not declared in the config, discovery will first use the
567
+ * wildcard target, if one is configured, and otherwise fall back to using
568
+ * the baseUrl with the provided `pathPattern` appended. The default path
569
+ * pattern is `"/api/{{ pluginId }}"`.
566
570
  */
567
571
  static fromConfig(config: Config, options?: {
568
572
  pathPattern?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/core-app-api",
3
- "version": "1.20.2",
3
+ "version": "1.20.3",
4
4
  "description": "Core app API used by Backstage apps",
5
5
  "backstage": {
6
6
  "role": "web-library"
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "files": [
40
40
  "dist",
41
- "config.d.ts"
41
+ "config.schema.json"
42
42
  ],
43
43
  "scripts": {
44
44
  "build": "backstage-cli package build",
@@ -51,9 +51,9 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@backstage/config": "^1.3.8",
54
- "@backstage/core-plugin-api": "^1.12.7",
54
+ "@backstage/core-plugin-api": "^1.12.8",
55
55
  "@backstage/types": "^1.2.2",
56
- "@backstage/ui": "^0.16.0",
56
+ "@backstage/ui": "^0.17.0",
57
57
  "@backstage/version-bridge": "^1.0.12",
58
58
  "@types/prop-types": "^15.7.3",
59
59
  "history": "^5.0.0",
@@ -65,8 +65,8 @@
65
65
  "zod": "^3.25.76 || ^4.0.0"
66
66
  },
67
67
  "devDependencies": {
68
- "@backstage/cli": "^0.36.3",
69
- "@backstage/test-utils": "^1.7.19",
68
+ "@backstage/cli": "^0.36.4",
69
+ "@backstage/test-utils": "^1.7.20",
70
70
  "@testing-library/dom": "^10.0.0",
71
71
  "@testing-library/jest-dom": "^6.0.0",
72
72
  "@testing-library/react": "^16.0.0",
@@ -94,6 +94,6 @@
94
94
  "optional": true
95
95
  }
96
96
  },
97
- "configSchema": "config.d.ts",
97
+ "configSchema": "config.schema.json",
98
98
  "module": "./dist/index.esm.js"
99
99
  }
package/config.d.ts DELETED
@@ -1,176 +0,0 @@
1
- /*
2
- * Copyright 2020 The Backstage Authors
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- export interface Config {
18
- /**
19
- * Generic frontend configuration.
20
- */
21
- app: {
22
- /**
23
- * The public absolute root URL that the frontend.
24
- * @visibility frontend
25
- */
26
- baseUrl: string;
27
-
28
- /**
29
- * The title of the app, as shown in the Backstage web interface.
30
- * @visibility frontend
31
- */
32
- title?: string;
33
-
34
- /**
35
- * Information about support of this Backstage instance and how to contact the integrator team.
36
- */
37
- support?: {
38
- /**
39
- * The primary support url.
40
- * @visibility frontend
41
- */
42
- url: string;
43
- /**
44
- * A list of categorized support item groupings.
45
- */
46
- items: Array<{
47
- /**
48
- * The title of the support item grouping.
49
- * @visibility frontend
50
- */
51
- title: string;
52
- /**
53
- * An optional icon for the support item grouping.
54
- * @visibility frontend
55
- */
56
- icon?: string;
57
- /**
58
- * A list of support links for the Backstage instance inside this grouping.
59
- */
60
- links: Array<{
61
- /** @visibility frontend */
62
- url: string;
63
- /** @visibility frontend */
64
- title?: string;
65
- }>;
66
- }>;
67
- };
68
-
69
- routes?: {
70
- /**
71
- * Maps external route references to regular route references. Both the
72
- * key and the value is expected to be on the form `<pluginId>.<routeId>`.
73
- * If the value is `false`, the route will be disabled even if it has a
74
- * default mapping.
75
- *
76
- * @deepVisibility frontend
77
- */
78
- bindings?: { [externalRouteRefId: string]: string | false };
79
- };
80
- };
81
-
82
- /**
83
- * Generic backend configuration.
84
- */
85
- backend: {
86
- /**
87
- * The public absolute root URL that the backend is reachable at, from the browser's perspective.
88
- * @visibility frontend
89
- */
90
- baseUrl: string;
91
- };
92
-
93
- /**
94
- * Configuration that provides information about the organization that the app is for.
95
- */
96
- organization?: {
97
- /**
98
- * The name of the organization that the app belongs to.
99
- * @visibility frontend
100
- */
101
- name?: string;
102
- };
103
-
104
- /**
105
- * This config was used by the HomepageTimer but has been replaced by the HeaderWorldClock in the home plugin
106
- *
107
- * @deprecated in favor of the HeaderWorldClock which is found in the home plugin
108
- */
109
- homepage?: {
110
- clocks?: Array<{
111
- /** @visibility frontend */
112
- label: string;
113
- /** @visibility frontend */
114
- timezone: string;
115
- }>;
116
- };
117
-
118
- /**
119
- * Configuration that provides information on available configured authentication providers.
120
- */
121
- auth?: {
122
- /**
123
- * The 'environment' attribute added as an optional parameter to have configurable environment value for `auth.providers`.
124
- * default value: 'development'
125
- * optional values: 'development' | 'production'
126
- * @visibility frontend
127
- */
128
- environment?: string;
129
- };
130
-
131
- /**
132
- * Enable redirect authentication flow type, instead of a popup for authentication.
133
- * @defaultValue false
134
- * @visibility frontend
135
- */
136
- enableExperimentalRedirectFlow?: boolean;
137
-
138
- /**
139
- * Discovery options.
140
- *
141
- * @visibility frontend
142
- */
143
- discovery?: {
144
- /**
145
- * Endpoints
146
- *
147
- * A list of target baseUrls and the associated plugins.
148
- *
149
- * @visibility frontend
150
- */
151
- endpoints?: Array<{
152
- /**
153
- * The target baseUrl to use for the plugin
154
- *
155
- * Can be either a string or an object with internal and external keys. (Internal is used for the backend, external for the frontend)
156
- * Targets with `{{pluginId}}` or `{{ pluginId }} in the url will be replaced with the pluginId.
157
- *
158
- * @visibility frontend
159
- */
160
- target:
161
- | string
162
- | {
163
- /**
164
- * @visibility frontend
165
- */
166
- external?: string;
167
- };
168
- /**
169
- * Array of plugins which use the target baseUrl.
170
- *
171
- * @visibility frontend
172
- */
173
- plugins: Array<string>;
174
- }>;
175
- };
176
- }