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

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,43 @@
1
1
  # @backstage/core-app-api
2
2
 
3
+ ## 0.5.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 40775bd263: Switched out the `GithubAuth` implementation to use the common `OAuth2` implementation. This relies on the simultaneous change in `@backstage/plugin-auth-backend` that enabled access token storage in cookies rather than the current solution that's based on `LocalStorage`.
8
+
9
+ > **NOTE:** Make sure you upgrade the `auth-backend` deployment before or at the same time as you deploy this change.
10
+
11
+ ## 0.5.2-next.0
12
+
13
+ ### Patch Changes
14
+
15
+ - 40775bd263: Switched out the `GithubAuth` implementation to use the common `OAuth2` implementation. This relies on the simultaneous change in `@backstage/plugin-auth-backend` that enabled access token storage in cookies rather than the current solution that's based on `LocalStorage`.
16
+
17
+ > **NOTE:** Make sure you upgrade the `auth-backend` deployment before or at the same time as you deploy this change.
18
+
19
+ ## 0.5.1
20
+
21
+ ### Patch Changes
22
+
23
+ - f959c22787: Asynchronous methods on the identity API can now reliably be called at any time, including early in the bootstrap process or prior to successful sign-in.
24
+
25
+ Previously in such situations, a `Tried to access IdentityApi before app was loaded` error would be thrown. Now, those methods will wait and resolve eventually (as soon as a concrete identity API is provided).
26
+
27
+ ## 0.5.0
28
+
29
+ ### Minor Changes
30
+
31
+ - ceebe25391: Removed deprecated `SignInResult` type, which was replaced with the new `onSignInSuccess` callback.
32
+
33
+ ### Patch Changes
34
+
35
+ - fb565073ec: Add an `allowUrl` callback option to `FetchMiddlewares.injectIdentityAuth`
36
+ - 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.
37
+ - Updated dependencies
38
+ - @backstage/core-plugin-api@0.6.0
39
+ - @backstage/config@0.1.13
40
+
3
41
  ## 0.5.0-next.0
4
42
 
5
43
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ReactNode, PropsWithChildren, ComponentType } from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { ApiHolder, ApiRef, ApiFactory, AnyApiRef, ProfileInfo, BackstageIdentityResponse, OAuthRequestApi, DiscoveryApi, AuthProviderInfo, OAuthApi, SessionApi, SessionState, AuthRequestOptions, gitlabAuthApiRef, googleAuthApiRef, OpenIdConnectApi, ProfileInfoApi, BackstageIdentityApi, oktaAuthApiRef, auth0AuthApiRef, microsoftAuthApiRef, oneloginAuthApiRef, bitbucketAuthApiRef, atlassianAuthApiRef, AlertApi, AlertMessage, AnalyticsApi, AnalyticsEvent, AppThemeApi, AppTheme, ErrorApi, ErrorApiError, ErrorApiErrorContext, FeatureFlagsApi, FeatureFlag, FeatureFlagsSaveOptions, FetchApi, IdentityApi, OAuthRequesterOptions, OAuthRequester, PendingOAuthRequest, StorageApi, StorageValueSnapshot, BackstagePlugin, IconComponent, ExternalRouteRef, AnyApiFactory, RouteRef, SubRouteRef } from '@backstage/core-plugin-api';
3
+ import { ApiHolder, ApiRef, ApiFactory, AnyApiRef, ProfileInfo, BackstageIdentityResponse, OAuthRequestApi, DiscoveryApi, AuthProviderInfo, githubAuthApiRef, gitlabAuthApiRef, googleAuthApiRef, OAuthApi, OpenIdConnectApi, ProfileInfoApi, BackstageIdentityApi, SessionApi, SessionState, AuthRequestOptions, oktaAuthApiRef, auth0AuthApiRef, microsoftAuthApiRef, oneloginAuthApiRef, bitbucketAuthApiRef, atlassianAuthApiRef, AlertApi, AlertMessage, AnalyticsApi, AnalyticsEvent, AppThemeApi, AppTheme, ErrorApi, ErrorApiError, ErrorApiErrorContext, FeatureFlagsApi, FeatureFlag, FeatureFlagsSaveOptions, FetchApi, IdentityApi, OAuthRequesterOptions, OAuthRequester, PendingOAuthRequest, StorageApi, StorageValueSnapshot, BackstagePlugin, IconComponent, ExternalRouteRef, AnyApiFactory, RouteRef, SubRouteRef } from '@backstage/core-plugin-api';
4
4
  import * as _backstage_types from '@backstage/types';
5
5
  import { Observable, JsonValue } from '@backstage/types';
6
6
  import { Config, AppConfig } from '@backstage/config';
@@ -130,16 +130,11 @@ declare type AuthApiCreateOptions = {
130
130
  *
131
131
  * @public
132
132
  */
133
- declare class GithubAuth implements OAuthApi, SessionApi {
134
- private readonly sessionManager;
135
- static create(options: OAuthApiCreateOptions): GithubAuth;
136
- private constructor();
137
- signIn(): Promise<void>;
138
- signOut(): Promise<void>;
139
- sessionState$(): Observable<SessionState>;
140
- getAccessToken(scope?: string, options?: AuthRequestOptions): Promise<string>;
141
- getBackstageIdentity(options?: AuthRequestOptions): Promise<BackstageIdentityResponse | undefined>;
142
- getProfile(options?: AuthRequestOptions): Promise<ProfileInfo | undefined>;
133
+ declare class GithubAuth {
134
+ static create(options: OAuthApiCreateOptions): typeof githubAuthApiRef.T;
135
+ /**
136
+ * @deprecated This method is deprecated and will be removed in a future release.
137
+ */
143
138
  static normalizeScope(scope?: string): Set<string>;
144
139
  }
145
140
 
@@ -510,14 +505,16 @@ declare class FetchMiddlewares {
510
505
  *
511
506
  * The header injection only happens on allowlisted URLs. Per default, if the
512
507
  * `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.
508
+ * the `urlPrefixAllowlist` or `allowUrl` options are passed in, in which case
509
+ * they take precedence. If you pass in neither config nor an
510
+ * allowlist/callback, the middleware will have no effect since effectively no
511
+ * request will match the (nonexistent) rules.
516
512
  */
517
513
  static injectIdentityAuth(options: {
518
514
  identityApi: IdentityApi;
519
515
  config?: Config;
520
516
  urlPrefixAllowlist?: string[];
517
+ allowUrl?: (url: string) => boolean;
521
518
  header?: {
522
519
  name: string;
523
520
  value: (backstageToken: string) => string;
package/dist/index.esm.js CHANGED
@@ -725,167 +725,7 @@ class AuthSessionStore {
725
725
  }
726
726
  }
727
727
 
728
- class OptionalRefreshSessionManagerMux {
729
- constructor(options) {
730
- this.stateTracker = new SessionStateTracker();
731
- this.sessionCanRefresh = options.sessionCanRefresh;
732
- this.staticSessionManager = options.staticSessionManager;
733
- this.refreshingSessionManager = options.refreshingSessionManager;
734
- }
735
- async getSession(options) {
736
- const staticSession = await this.staticSessionManager.getSession({
737
- ...options,
738
- optional: true
739
- });
740
- if (staticSession) {
741
- this.stateTracker.setIsSignedIn(true);
742
- return staticSession;
743
- }
744
- const session = await this.refreshingSessionManager.getSession(options);
745
- if (!session) {
746
- this.stateTracker.setIsSignedIn(false);
747
- return void 0;
748
- }
749
- if (this.sessionCanRefresh(session)) {
750
- this.stateTracker.setIsSignedIn(true);
751
- return session;
752
- }
753
- this.staticSessionManager.setSession(session);
754
- this.stateTracker.setIsSignedIn(true);
755
- return session;
756
- }
757
- async removeSession() {
758
- await Promise.all([
759
- this.refreshingSessionManager.removeSession(),
760
- this.staticSessionManager.removeSession()
761
- ]);
762
- this.stateTracker.setIsSignedIn(false);
763
- }
764
- sessionState$() {
765
- return this.stateTracker.sessionState$();
766
- }
767
- }
768
-
769
- const githubSessionSchema = z.object({
770
- providerInfo: z.object({
771
- accessToken: z.string(),
772
- scopes: z.set(z.string()),
773
- expiresAt: z.date().optional()
774
- }),
775
- profile: z.object({
776
- email: z.string().optional(),
777
- displayName: z.string().optional(),
778
- picture: z.string().optional()
779
- }),
780
- backstageIdentity: z.object({
781
- id: z.string(),
782
- token: z.string(),
783
- identity: z.object({
784
- type: z.literal("user"),
785
- userEntityRef: z.string(),
786
- ownershipEntityRefs: z.array(z.string())
787
- })
788
- })
789
- });
790
-
791
728
  const DEFAULT_PROVIDER$a = {
792
- id: "github",
793
- title: "GitHub",
794
- icon: () => null
795
- };
796
- class GithubAuth {
797
- constructor(sessionManager) {
798
- this.sessionManager = sessionManager;
799
- }
800
- static create(options) {
801
- const {
802
- discoveryApi,
803
- environment = "development",
804
- provider = DEFAULT_PROVIDER$a,
805
- oauthRequestApi,
806
- defaultScopes = ["read:user"]
807
- } = options;
808
- const connector = new DefaultAuthConnector({
809
- discoveryApi,
810
- environment,
811
- provider,
812
- oauthRequestApi,
813
- sessionTransform(res) {
814
- return {
815
- ...res,
816
- providerInfo: {
817
- accessToken: res.providerInfo.accessToken,
818
- scopes: GithubAuth.normalizeScope(res.providerInfo.scope),
819
- expiresAt: res.providerInfo.expiresInSeconds ? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1e3) : void 0
820
- }
821
- };
822
- }
823
- });
824
- const refreshingSessionManager = new RefreshingAuthSessionManager({
825
- connector,
826
- defaultScopes: new Set(defaultScopes),
827
- sessionScopes: (session) => session.providerInfo.scopes,
828
- sessionShouldRefresh: (session) => {
829
- const { expiresAt } = session.providerInfo;
830
- if (!expiresAt) {
831
- return false;
832
- }
833
- const expiresInSec = (expiresAt.getTime() - Date.now()) / 1e3;
834
- return expiresInSec < 60 * 5;
835
- }
836
- });
837
- const staticSessionManager = new AuthSessionStore({
838
- manager: new StaticAuthSessionManager({
839
- connector,
840
- defaultScopes: new Set(defaultScopes),
841
- sessionScopes: (session) => session.providerInfo.scopes
842
- }),
843
- storageKey: `${provider.id}Session`,
844
- schema: githubSessionSchema,
845
- sessionScopes: (session) => session.providerInfo.scopes
846
- });
847
- const sessionManagerMux = new OptionalRefreshSessionManagerMux({
848
- refreshingSessionManager,
849
- staticSessionManager,
850
- sessionCanRefresh: (session) => session.providerInfo.expiresAt !== void 0
851
- });
852
- return new GithubAuth(sessionManagerMux);
853
- }
854
- async signIn() {
855
- await this.getAccessToken();
856
- }
857
- async signOut() {
858
- await this.sessionManager.removeSession();
859
- }
860
- sessionState$() {
861
- return this.sessionManager.sessionState$();
862
- }
863
- async getAccessToken(scope, options) {
864
- var _a;
865
- const session = await this.sessionManager.getSession({
866
- ...options,
867
- scopes: GithubAuth.normalizeScope(scope)
868
- });
869
- return (_a = session == null ? void 0 : session.providerInfo.accessToken) != null ? _a : "";
870
- }
871
- async getBackstageIdentity(options = {}) {
872
- const session = await this.sessionManager.getSession(options);
873
- return session == null ? void 0 : session.backstageIdentity;
874
- }
875
- async getProfile(options = {}) {
876
- const session = await this.sessionManager.getSession(options);
877
- return session == null ? void 0 : session.profile;
878
- }
879
- static normalizeScope(scope) {
880
- if (!scope) {
881
- return /* @__PURE__ */ new Set();
882
- }
883
- const scopeList = Array.isArray(scope) ? scope : scope.split(/[\s|,]/).filter(Boolean);
884
- return new Set(scopeList);
885
- }
886
- }
887
-
888
- const DEFAULT_PROVIDER$9 = {
889
729
  id: "oauth2",
890
730
  title: "Your Identity Provider",
891
731
  icon: () => null
@@ -895,7 +735,7 @@ class OAuth2 {
895
735
  const {
896
736
  discoveryApi,
897
737
  environment = "development",
898
- provider = DEFAULT_PROVIDER$9,
738
+ provider = DEFAULT_PROVIDER$a,
899
739
  oauthRequestApi,
900
740
  defaultScopes = [],
901
741
  scopeTransform = (x) => x
@@ -972,6 +812,37 @@ class OAuth2 {
972
812
  }
973
813
  }
974
814
 
815
+ const DEFAULT_PROVIDER$9 = {
816
+ id: "github",
817
+ title: "GitHub",
818
+ icon: () => null
819
+ };
820
+ class GithubAuth {
821
+ static create(options) {
822
+ const {
823
+ discoveryApi,
824
+ environment = "development",
825
+ provider = DEFAULT_PROVIDER$9,
826
+ oauthRequestApi,
827
+ defaultScopes = ["read:user"]
828
+ } = options;
829
+ return OAuth2.create({
830
+ discoveryApi,
831
+ oauthRequestApi,
832
+ provider,
833
+ environment,
834
+ defaultScopes
835
+ });
836
+ }
837
+ static normalizeScope(scope) {
838
+ if (!scope) {
839
+ return /* @__PURE__ */ new Set();
840
+ }
841
+ const scopeList = Array.isArray(scope) ? scope : scope.split(/[\s|,]/).filter(Boolean);
842
+ return new Set(scopeList);
843
+ }
844
+ }
845
+
975
846
  const DEFAULT_PROVIDER$8 = {
976
847
  id: "gitlab",
977
848
  title: "GitLab",
@@ -1497,29 +1368,24 @@ function createFetchApi(options) {
1497
1368
  }
1498
1369
 
1499
1370
  class IdentityAuthInjectorFetchMiddleware {
1500
- constructor(identityApi, urlPrefixAllowlist, headerName, headerValue) {
1371
+ constructor(identityApi, allowUrl, headerName, headerValue) {
1501
1372
  this.identityApi = identityApi;
1502
- this.urlPrefixAllowlist = urlPrefixAllowlist;
1373
+ this.allowUrl = allowUrl;
1503
1374
  this.headerName = headerName;
1504
1375
  this.headerValue = headerValue;
1505
1376
  }
1506
1377
  static create(options) {
1507
1378
  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
- }
1379
+ const matcher = buildMatcher(options);
1514
1380
  const headerName = ((_a = options.header) == null ? void 0 : _a.name) || "authorization";
1515
1381
  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);
1382
+ return new IdentityAuthInjectorFetchMiddleware(options.identityApi, matcher, headerName, headerValue);
1517
1383
  }
1518
1384
  apply(next) {
1519
1385
  return async (input, init) => {
1520
1386
  const request = new Request(input, init);
1521
1387
  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) {
1388
+ if (request.headers.get(this.headerName) || typeof token !== "string" || !token || !this.allowUrl(request.url)) {
1523
1389
  return next(input, init);
1524
1390
  }
1525
1391
  request.headers.set(this.headerName, this.headerValue(token));
@@ -1527,6 +1393,20 @@ class IdentityAuthInjectorFetchMiddleware {
1527
1393
  };
1528
1394
  }
1529
1395
  }
1396
+ function buildMatcher(options) {
1397
+ if (options.allowUrl) {
1398
+ return options.allowUrl;
1399
+ } else if (options.urlPrefixAllowlist) {
1400
+ return buildPrefixMatcher(options.urlPrefixAllowlist);
1401
+ } else if (options.config) {
1402
+ return buildPrefixMatcher([options.config.getString("backend.baseUrl")]);
1403
+ }
1404
+ return () => false;
1405
+ }
1406
+ function buildPrefixMatcher(prefixes) {
1407
+ const trimmedPrefixes = prefixes.map((prefix) => prefix.replace(/\/$/, ""));
1408
+ return (url) => trimmedPrefixes.some((prefix) => url === prefix || url.startsWith(`${prefix}/`));
1409
+ }
1530
1410
 
1531
1411
  function join(left, right) {
1532
1412
  if (!right || right === "/") {
@@ -2138,7 +2018,7 @@ const RouteTracker = ({ tree }) => {
2138
2018
  }));
2139
2019
  };
2140
2020
 
2141
- function validateRoutes(routePaths, routeParents) {
2021
+ function validateRouteParameters(routePaths, routeParents) {
2142
2022
  const notLeafRoutes = new Set(routeParents.values());
2143
2023
  notLeafRoutes.delete(void 0);
2144
2024
  for (const route of routeParents.keys()) {
@@ -2167,6 +2047,21 @@ function validateRoutes(routePaths, routeParents) {
2167
2047
  }
2168
2048
  }
2169
2049
  }
2050
+ function validateRouteBindings(routeBindings, plugins) {
2051
+ for (const plugin of plugins) {
2052
+ if (!plugin.externalRoutes) {
2053
+ continue;
2054
+ }
2055
+ for (const [name, externalRouteRef] of Object.entries(plugin.externalRoutes)) {
2056
+ if (externalRouteRef.optional) {
2057
+ continue;
2058
+ }
2059
+ if (!routeBindings.has(externalRouteRef)) {
2060
+ 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.`);
2061
+ }
2062
+ }
2063
+ }
2064
+ }
2170
2065
 
2171
2066
  const AppContext = createVersionedContext("app-context");
2172
2067
  const AppContextProvider = ({
@@ -2187,8 +2082,16 @@ function logDeprecation(thing) {
2187
2082
  console.warn(`WARNING: Call to ${thing} is deprecated and will break in the future`);
2188
2083
  }
2189
2084
  class AppIdentityProxy {
2085
+ constructor() {
2086
+ this.resolveTarget = () => {
2087
+ };
2088
+ this.waitForTarget = new Promise((resolve) => {
2089
+ this.resolveTarget = resolve;
2090
+ });
2091
+ }
2190
2092
  setTarget(identityApi) {
2191
2093
  this.target = identityApi;
2094
+ this.resolveTarget(identityApi);
2192
2095
  }
2193
2096
  getUserId() {
2194
2097
  if (!this.target) {
@@ -2211,42 +2114,29 @@ class AppIdentityProxy {
2211
2114
  return this.target.getProfile();
2212
2115
  }
2213
2116
  async getProfileInfo() {
2214
- if (!this.target) {
2215
- throw mkError("getProfileInfo");
2216
- }
2217
- return this.target.getProfileInfo();
2117
+ return this.waitForTarget.then((target) => target.getProfileInfo());
2218
2118
  }
2219
2119
  async getBackstageIdentity() {
2220
- if (!this.target) {
2221
- throw mkError("getBackstageIdentity");
2222
- }
2223
- const identity = await this.target.getBackstageIdentity();
2120
+ const identity = await this.waitForTarget.then((target) => target.getBackstageIdentity());
2224
2121
  if (!identity.userEntityRef.match(/^.*:.*\/.*$/)) {
2225
2122
  console.warn(`WARNING: The App IdentityApi provided an invalid userEntityRef, '${identity.userEntityRef}'. It must be a full Entity Reference of the form '<kind>:<namespace>/<name>'.`);
2226
2123
  }
2227
2124
  return identity;
2228
2125
  }
2229
2126
  async getCredentials() {
2230
- if (!this.target) {
2231
- throw mkError("getCredentials");
2232
- }
2233
- return this.target.getCredentials();
2127
+ return this.waitForTarget.then((target) => target.getCredentials());
2234
2128
  }
2235
2129
  async getIdToken() {
2236
- if (!this.target) {
2237
- throw mkError("getIdToken");
2238
- }
2239
- if (!this.target.getIdToken) {
2240
- throw new Error("IdentityApi does not implement getIdToken");
2241
- }
2242
- logDeprecation("getIdToken");
2243
- return this.target.getIdToken();
2130
+ return this.waitForTarget.then((target) => {
2131
+ if (!target.getIdToken) {
2132
+ throw new Error("IdentityApi does not implement getIdToken");
2133
+ }
2134
+ logDeprecation("getIdToken");
2135
+ return target.getIdToken();
2136
+ });
2244
2137
  }
2245
2138
  async signOut() {
2246
- if (!this.target) {
2247
- throw mkError("signOut");
2248
- }
2249
- await this.target.signOut();
2139
+ await this.waitForTarget.then((target) => target.signOut());
2250
2140
  location.reload();
2251
2141
  }
2252
2142
  }
@@ -2361,7 +2251,7 @@ class ApiRegistry {
2361
2251
  }
2362
2252
  }
2363
2253
 
2364
- function generateBoundRoutes(bindRoutes) {
2254
+ function resolveRouteBindings(bindRoutes) {
2365
2255
  const result = /* @__PURE__ */ new Map();
2366
2256
  if (bindRoutes) {
2367
2257
  const bind = (externalRoutes, targetRoutes) => {
@@ -2382,6 +2272,7 @@ function generateBoundRoutes(bindRoutes) {
2382
2272
  }
2383
2273
  return result;
2384
2274
  }
2275
+
2385
2276
  function getBasePath(configApi) {
2386
2277
  var _a;
2387
2278
  let { pathname } = new URL((_a = configApi.getOptionalString("app.baseUrl")) != null ? _a : "/", "http://dummy.dev");
@@ -2453,9 +2344,16 @@ class AppManager {
2453
2344
  }
2454
2345
  getProvider() {
2455
2346
  const appContext = new AppContextImpl(this);
2347
+ let routesHaveBeenValidated = false;
2456
2348
  const Provider = ({ children }) => {
2457
2349
  const appThemeApi = useMemo(() => AppThemeSelector.createWithStorage(this.themes), []);
2458
- const { routePaths, routeParents, routeObjects, featureFlags } = useMemo(() => {
2350
+ const {
2351
+ routePaths,
2352
+ routeParents,
2353
+ routeObjects,
2354
+ featureFlags,
2355
+ routeBindings
2356
+ } = useMemo(() => {
2459
2357
  const result = traverseElementTree({
2460
2358
  root: children,
2461
2359
  discoverers: [childDiscoverer, routeElementDiscoverer],
@@ -2467,12 +2365,19 @@ class AppManager {
2467
2365
  featureFlags: featureFlagCollector
2468
2366
  }
2469
2367
  });
2470
- validateRoutes(result.routePaths, result.routeParents);
2471
2368
  result.collectedPlugins.forEach((plugin) => this.plugins.add(plugin));
2472
2369
  this.verifyPlugins(this.plugins);
2473
2370
  this.getApiHolder();
2474
- return result;
2371
+ return {
2372
+ ...result,
2373
+ routeBindings: resolveRouteBindings(this.bindRoutes)
2374
+ };
2475
2375
  }, [children]);
2376
+ if (!routesHaveBeenValidated) {
2377
+ routesHaveBeenValidated = true;
2378
+ validateRouteParameters(routePaths, routeParents);
2379
+ validateRouteBindings(routeBindings, this.plugins);
2380
+ }
2476
2381
  const loadedConfig = useConfigLoader(this.configLoader, this.components, appThemeApi);
2477
2382
  const hasConfigApi = "api" in loadedConfig;
2478
2383
  if (hasConfigApi) {
@@ -2518,7 +2423,7 @@ class AppManager {
2518
2423
  routePaths,
2519
2424
  routeParents,
2520
2425
  routeObjects,
2521
- routeBindings: generateBoundRoutes(this.bindRoutes),
2426
+ routeBindings,
2522
2427
  basePath: getBasePath(loadedConfig.api)
2523
2428
  }, children))));
2524
2429
  };