@backstage/core-app-api 0.5.0-next.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @backstage/core-app-api
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ceebe25391: Removed deprecated `SignInResult` type, which was replaced with the new `onSignInSuccess` callback.
8
+
9
+ ### Patch Changes
10
+
11
+ - fb565073ec: Add an `allowUrl` callback option to `FetchMiddlewares.injectIdentityAuth`
12
+ - f050eec2c0: Added validation during the application startup that detects if there are any plugins present that have not had their required external routes bound. Failing the validation will cause a hard crash as it is a programmer error. It lets you detect early on that there are dangling routes, rather than having them cause an error later on.
13
+ - Updated dependencies
14
+ - @backstage/core-plugin-api@0.6.0
15
+ - @backstage/config@0.1.13
16
+
3
17
  ## 0.5.0-next.0
4
18
 
5
19
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -510,14 +510,16 @@ declare class FetchMiddlewares {
510
510
  *
511
511
  * The header injection only happens on allowlisted URLs. Per default, if the
512
512
  * `config` option is passed in, the `backend.baseUrl` is allowlisted, unless
513
- * the `urlPrefixAllowlist` option is passed in, in which case it takes
514
- * precedence. If you pass in neither config nor an allowlist, the middleware
515
- * will have no effect.
513
+ * the `urlPrefixAllowlist` or `allowUrl` options are passed in, in which case
514
+ * they take precedence. If you pass in neither config nor an
515
+ * allowlist/callback, the middleware will have no effect since effectively no
516
+ * request will match the (nonexistent) rules.
516
517
  */
517
518
  static injectIdentityAuth(options: {
518
519
  identityApi: IdentityApi;
519
520
  config?: Config;
520
521
  urlPrefixAllowlist?: string[];
522
+ allowUrl?: (url: string) => boolean;
521
523
  header?: {
522
524
  name: string;
523
525
  value: (backstageToken: string) => string;
package/dist/index.esm.js CHANGED
@@ -1497,29 +1497,24 @@ function createFetchApi(options) {
1497
1497
  }
1498
1498
 
1499
1499
  class IdentityAuthInjectorFetchMiddleware {
1500
- constructor(identityApi, urlPrefixAllowlist, headerName, headerValue) {
1500
+ constructor(identityApi, allowUrl, headerName, headerValue) {
1501
1501
  this.identityApi = identityApi;
1502
- this.urlPrefixAllowlist = urlPrefixAllowlist;
1502
+ this.allowUrl = allowUrl;
1503
1503
  this.headerName = headerName;
1504
1504
  this.headerValue = headerValue;
1505
1505
  }
1506
1506
  static create(options) {
1507
1507
  var _a, _b;
1508
- const allowlist = [];
1509
- if (options.urlPrefixAllowlist) {
1510
- allowlist.push(...options.urlPrefixAllowlist);
1511
- } else if (options.config) {
1512
- allowlist.push(options.config.getString("backend.baseUrl"));
1513
- }
1508
+ const matcher = buildMatcher(options);
1514
1509
  const headerName = ((_a = options.header) == null ? void 0 : _a.name) || "authorization";
1515
1510
  const headerValue = ((_b = options.header) == null ? void 0 : _b.value) || ((token) => `Bearer ${token}`);
1516
- return new IdentityAuthInjectorFetchMiddleware(options.identityApi, allowlist.map((prefix) => prefix.replace(/\/$/, "")), headerName, headerValue);
1511
+ return new IdentityAuthInjectorFetchMiddleware(options.identityApi, matcher, headerName, headerValue);
1517
1512
  }
1518
1513
  apply(next) {
1519
1514
  return async (input, init) => {
1520
1515
  const request = new Request(input, init);
1521
1516
  const { token } = await this.identityApi.getCredentials();
1522
- if (request.headers.get(this.headerName) || !this.urlPrefixAllowlist.some((prefix) => request.url === prefix || request.url.startsWith(`${prefix}/`)) || typeof token !== "string" || !token) {
1517
+ if (request.headers.get(this.headerName) || typeof token !== "string" || !token || !this.allowUrl(request.url)) {
1523
1518
  return next(input, init);
1524
1519
  }
1525
1520
  request.headers.set(this.headerName, this.headerValue(token));
@@ -1527,6 +1522,20 @@ class IdentityAuthInjectorFetchMiddleware {
1527
1522
  };
1528
1523
  }
1529
1524
  }
1525
+ function buildMatcher(options) {
1526
+ if (options.allowUrl) {
1527
+ return options.allowUrl;
1528
+ } else if (options.urlPrefixAllowlist) {
1529
+ return buildPrefixMatcher(options.urlPrefixAllowlist);
1530
+ } else if (options.config) {
1531
+ return buildPrefixMatcher([options.config.getString("backend.baseUrl")]);
1532
+ }
1533
+ return () => false;
1534
+ }
1535
+ function buildPrefixMatcher(prefixes) {
1536
+ const trimmedPrefixes = prefixes.map((prefix) => prefix.replace(/\/$/, ""));
1537
+ return (url) => trimmedPrefixes.some((prefix) => url === prefix || url.startsWith(`${prefix}/`));
1538
+ }
1530
1539
 
1531
1540
  function join(left, right) {
1532
1541
  if (!right || right === "/") {
@@ -2138,7 +2147,7 @@ const RouteTracker = ({ tree }) => {
2138
2147
  }));
2139
2148
  };
2140
2149
 
2141
- function validateRoutes(routePaths, routeParents) {
2150
+ function validateRouteParameters(routePaths, routeParents) {
2142
2151
  const notLeafRoutes = new Set(routeParents.values());
2143
2152
  notLeafRoutes.delete(void 0);
2144
2153
  for (const route of routeParents.keys()) {
@@ -2167,6 +2176,21 @@ function validateRoutes(routePaths, routeParents) {
2167
2176
  }
2168
2177
  }
2169
2178
  }
2179
+ function validateRouteBindings(routeBindings, plugins) {
2180
+ for (const plugin of plugins) {
2181
+ if (!plugin.externalRoutes) {
2182
+ continue;
2183
+ }
2184
+ for (const [name, externalRouteRef] of Object.entries(plugin.externalRoutes)) {
2185
+ if (externalRouteRef.optional) {
2186
+ continue;
2187
+ }
2188
+ if (!routeBindings.has(externalRouteRef)) {
2189
+ throw new Error(`External route '${name}' of the '${plugin.getId()}' plugin must be bound to a target route. See https://backstage.io/link?bind-routes for details.`);
2190
+ }
2191
+ }
2192
+ }
2193
+ }
2170
2194
 
2171
2195
  const AppContext = createVersionedContext("app-context");
2172
2196
  const AppContextProvider = ({
@@ -2361,7 +2385,7 @@ class ApiRegistry {
2361
2385
  }
2362
2386
  }
2363
2387
 
2364
- function generateBoundRoutes(bindRoutes) {
2388
+ function resolveRouteBindings(bindRoutes) {
2365
2389
  const result = /* @__PURE__ */ new Map();
2366
2390
  if (bindRoutes) {
2367
2391
  const bind = (externalRoutes, targetRoutes) => {
@@ -2382,6 +2406,7 @@ function generateBoundRoutes(bindRoutes) {
2382
2406
  }
2383
2407
  return result;
2384
2408
  }
2409
+
2385
2410
  function getBasePath(configApi) {
2386
2411
  var _a;
2387
2412
  let { pathname } = new URL((_a = configApi.getOptionalString("app.baseUrl")) != null ? _a : "/", "http://dummy.dev");
@@ -2453,9 +2478,16 @@ class AppManager {
2453
2478
  }
2454
2479
  getProvider() {
2455
2480
  const appContext = new AppContextImpl(this);
2481
+ let routesHaveBeenValidated = false;
2456
2482
  const Provider = ({ children }) => {
2457
2483
  const appThemeApi = useMemo(() => AppThemeSelector.createWithStorage(this.themes), []);
2458
- const { routePaths, routeParents, routeObjects, featureFlags } = useMemo(() => {
2484
+ const {
2485
+ routePaths,
2486
+ routeParents,
2487
+ routeObjects,
2488
+ featureFlags,
2489
+ routeBindings
2490
+ } = useMemo(() => {
2459
2491
  const result = traverseElementTree({
2460
2492
  root: children,
2461
2493
  discoverers: [childDiscoverer, routeElementDiscoverer],
@@ -2467,12 +2499,19 @@ class AppManager {
2467
2499
  featureFlags: featureFlagCollector
2468
2500
  }
2469
2501
  });
2470
- validateRoutes(result.routePaths, result.routeParents);
2471
2502
  result.collectedPlugins.forEach((plugin) => this.plugins.add(plugin));
2472
2503
  this.verifyPlugins(this.plugins);
2473
2504
  this.getApiHolder();
2474
- return result;
2505
+ return {
2506
+ ...result,
2507
+ routeBindings: resolveRouteBindings(this.bindRoutes)
2508
+ };
2475
2509
  }, [children]);
2510
+ if (!routesHaveBeenValidated) {
2511
+ routesHaveBeenValidated = true;
2512
+ validateRouteParameters(routePaths, routeParents);
2513
+ validateRouteBindings(routeBindings, this.plugins);
2514
+ }
2476
2515
  const loadedConfig = useConfigLoader(this.configLoader, this.components, appThemeApi);
2477
2516
  const hasConfigApi = "api" in loadedConfig;
2478
2517
  if (hasConfigApi) {
@@ -2518,7 +2557,7 @@ class AppManager {
2518
2557
  routePaths,
2519
2558
  routeParents,
2520
2559
  routeObjects,
2521
- routeBindings: generateBoundRoutes(this.bindRoutes),
2560
+ routeBindings,
2522
2561
  basePath: getBasePath(loadedConfig.api)
2523
2562
  }, children))));
2524
2563
  };