@backstage/core-app-api 0.4.0 → 0.5.2-next.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,47 @@
1
1
  # @backstage/core-app-api
2
2
 
3
+ ## 0.5.2-next.0
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.1
12
+
13
+ ### Patch Changes
14
+
15
+ - 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.
16
+
17
+ 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).
18
+
19
+ ## 0.5.0
20
+
21
+ ### Minor Changes
22
+
23
+ - ceebe25391: Removed deprecated `SignInResult` type, which was replaced with the new `onSignInSuccess` callback.
24
+
25
+ ### Patch Changes
26
+
27
+ - fb565073ec: Add an `allowUrl` callback option to `FetchMiddlewares.injectIdentityAuth`
28
+ - 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.
29
+ - Updated dependencies
30
+ - @backstage/core-plugin-api@0.6.0
31
+ - @backstage/config@0.1.13
32
+
33
+ ## 0.5.0-next.0
34
+
35
+ ### Minor Changes
36
+
37
+ - ceebe25391: Removed deprecated `SignInResult` type, which was replaced with the new `onSignInSuccess` callback.
38
+
39
+ ### Patch Changes
40
+
41
+ - Updated dependencies
42
+ - @backstage/core-plugin-api@0.6.0-next.0
43
+ - @backstage/config@0.1.13-next.0
44
+
3
45
  ## 0.4.0
4
46
 
5
47
  ### 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, BackstageIdentity, 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';
@@ -104,7 +104,7 @@ declare type GithubSession = {
104
104
  expiresAt?: Date;
105
105
  };
106
106
  profile: ProfileInfo;
107
- backstageIdentity: BackstageIdentity;
107
+ backstageIdentity: BackstageIdentityResponse;
108
108
  };
109
109
 
110
110
  /**
@@ -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<BackstageIdentity | 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
 
@@ -183,7 +178,7 @@ declare class OAuth2 implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, Back
183
178
  sessionState$(): Observable<SessionState>;
184
179
  getAccessToken(scope?: string | string[], options?: AuthRequestOptions): Promise<string>;
185
180
  getIdToken(options?: AuthRequestOptions): Promise<string>;
186
- getBackstageIdentity(options?: AuthRequestOptions): Promise<BackstageIdentity | undefined>;
181
+ getBackstageIdentity(options?: AuthRequestOptions): Promise<BackstageIdentityResponse | undefined>;
187
182
  getProfile(options?: AuthRequestOptions): Promise<ProfileInfo | undefined>;
188
183
  private static normalizeScopes;
189
184
  }
@@ -201,7 +196,7 @@ declare type OAuth2Session = {
201
196
  expiresAt: Date;
202
197
  };
203
198
  profile: ProfileInfo;
204
- backstageIdentity: BackstageIdentity;
199
+ backstageIdentity: BackstageIdentityResponse;
205
200
  };
206
201
 
207
202
  /**
@@ -225,7 +220,7 @@ declare class SamlAuth implements ProfileInfoApi, BackstageIdentityApi, SessionA
225
220
  private constructor();
226
221
  signIn(): Promise<void>;
227
222
  signOut(): Promise<void>;
228
- getBackstageIdentity(options?: AuthRequestOptions): Promise<BackstageIdentity | undefined>;
223
+ getBackstageIdentity(options?: AuthRequestOptions): Promise<BackstageIdentityResponse | undefined>;
229
224
  getProfile(options?: AuthRequestOptions): Promise<ProfileInfo | undefined>;
230
225
  }
231
226
 
@@ -238,7 +233,7 @@ declare class SamlAuth implements ProfileInfoApi, BackstageIdentityApi, SessionA
238
233
  declare type ExportedSamlSession = {
239
234
  userId: string;
240
235
  profile: ProfileInfo;
241
- backstageIdentity: BackstageIdentity;
236
+ backstageIdentity: BackstageIdentityResponse;
242
237
  };
243
238
 
244
239
  /**
@@ -307,7 +302,7 @@ declare type BitbucketSession = {
307
302
  expiresAt?: Date;
308
303
  };
309
304
  profile: ProfileInfo;
310
- backstageIdentity: BackstageIdentity;
305
+ backstageIdentity: BackstageIdentityResponse;
311
306
  };
312
307
 
313
308
  /**
@@ -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;
@@ -578,27 +575,6 @@ declare type BootErrorPageProps = {
578
575
  step: 'load-config' | 'load-chunk';
579
576
  error: Error;
580
577
  };
581
- /**
582
- * The outcome of signing in on the sign-in page.
583
- *
584
- * @public
585
- * @deprecated replaced by passing the {@link @backstage/core-plugin-api#IdentityApi} to the {@link SignInPageProps.onSignInSuccess} instead.
586
- */
587
- declare type SignInResult = {
588
- /**
589
- * User ID that will be returned by the IdentityApi
590
- */
591
- userId: string;
592
- profile: ProfileInfo;
593
- /**
594
- * Function used to retrieve an ID token for the signed in user.
595
- */
596
- getIdToken?: () => Promise<string>;
597
- /**
598
- * Sign out handler that will be called if the user requests to sign out.
599
- */
600
- signOut?: () => Promise<void>;
601
- };
602
578
  /**
603
579
  * Props for the `SignInPage` component of {@link AppComponents}.
604
580
  *
@@ -937,4 +913,4 @@ declare type FeatureFlaggedProps = {
937
913
  */
938
914
  declare const FeatureFlagged: (props: FeatureFlaggedProps) => JSX.Element;
939
915
 
940
- export { AlertApiForwarder, ApiFactoryHolder, ApiFactoryRegistry, ApiFactoryScope, ApiProvider, ApiProviderProps, ApiResolver, AppComponents, AppConfigLoader, AppContext, AppIcons, AppOptions, AppRouteBinder, AppThemeSelector, AtlassianAuth, Auth0Auth, AuthApiCreateOptions, BackstageApp, BitbucketAuth, BitbucketSession, BootErrorPageProps, ErrorAlerter, ErrorApiForwarder, ErrorBoundaryFallbackProps, FeatureFlagged, FeatureFlaggedProps, FetchMiddleware, FetchMiddlewares, FlatRoutes, FlatRoutesProps, GithubAuth, GithubSession, GitlabAuth, GoogleAuth, LocalStorageFeatureFlags, MicrosoftAuth, NoOpAnalyticsApi, OAuth2, OAuth2CreateOptions, OAuth2Session, OAuthApiCreateOptions, OAuthRequestManager, OktaAuth, OneLoginAuth, OneLoginAuthCreateOptions, SamlAuth, ExportedSamlSession as SamlSession, SignInPageProps, SignInResult, UnhandledErrorForwarder, UrlPatternDiscovery, WebStorage, createFetchApi, createSpecializedApp, defaultConfigLoader };
916
+ export { AlertApiForwarder, ApiFactoryHolder, ApiFactoryRegistry, ApiFactoryScope, ApiProvider, ApiProviderProps, ApiResolver, AppComponents, AppConfigLoader, AppContext, AppIcons, AppOptions, AppRouteBinder, AppThemeSelector, AtlassianAuth, Auth0Auth, AuthApiCreateOptions, BackstageApp, BitbucketAuth, BitbucketSession, BootErrorPageProps, ErrorAlerter, ErrorApiForwarder, ErrorBoundaryFallbackProps, FeatureFlagged, FeatureFlaggedProps, FetchMiddleware, FetchMiddlewares, FlatRoutes, FlatRoutesProps, GithubAuth, GithubSession, GitlabAuth, GoogleAuth, LocalStorageFeatureFlags, MicrosoftAuth, NoOpAnalyticsApi, OAuth2, OAuth2CreateOptions, OAuth2Session, OAuthApiCreateOptions, OAuthRequestManager, OktaAuth, OneLoginAuth, OneLoginAuthCreateOptions, SamlAuth, ExportedSamlSession as SamlSession, SignInPageProps, UnhandledErrorForwarder, UrlPatternDiscovery, WebStorage, createFetchApi, createSpecializedApp, defaultConfigLoader };
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 = ({
@@ -2183,51 +2078,65 @@ const AppContextProvider = ({
2183
2078
  function mkError(thing) {
2184
2079
  return new Error(`Tried to access IdentityApi ${thing} before app was loaded`);
2185
2080
  }
2081
+ function logDeprecation(thing) {
2082
+ console.warn(`WARNING: Call to ${thing} is deprecated and will break in the future`);
2083
+ }
2186
2084
  class AppIdentityProxy {
2085
+ constructor() {
2086
+ this.resolveTarget = () => {
2087
+ };
2088
+ this.waitForTarget = new Promise((resolve) => {
2089
+ this.resolveTarget = resolve;
2090
+ });
2091
+ }
2187
2092
  setTarget(identityApi) {
2188
2093
  this.target = identityApi;
2094
+ this.resolveTarget(identityApi);
2189
2095
  }
2190
2096
  getUserId() {
2191
2097
  if (!this.target) {
2192
2098
  throw mkError("getUserId");
2193
2099
  }
2100
+ if (!this.target.getUserId) {
2101
+ throw new Error("IdentityApi does not implement getUserId");
2102
+ }
2103
+ logDeprecation("getUserId");
2194
2104
  return this.target.getUserId();
2195
2105
  }
2196
2106
  getProfile() {
2197
2107
  if (!this.target) {
2198
2108
  throw mkError("getProfile");
2199
2109
  }
2110
+ if (!this.target.getProfile) {
2111
+ throw new Error("IdentityApi does not implement getProfile");
2112
+ }
2113
+ logDeprecation("getProfile");
2200
2114
  return this.target.getProfile();
2201
2115
  }
2202
2116
  async getProfileInfo() {
2203
- if (!this.target) {
2204
- throw mkError("getProfileInfo");
2205
- }
2206
- return this.target.getProfileInfo();
2117
+ return this.waitForTarget.then((target) => target.getProfileInfo());
2207
2118
  }
2208
2119
  async getBackstageIdentity() {
2209
- if (!this.target) {
2210
- throw mkError("getBackstageIdentity");
2120
+ const identity = await this.waitForTarget.then((target) => target.getBackstageIdentity());
2121
+ if (!identity.userEntityRef.match(/^.*:.*\/.*$/)) {
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>'.`);
2211
2123
  }
2212
- return this.target.getBackstageIdentity();
2124
+ return identity;
2213
2125
  }
2214
2126
  async getCredentials() {
2215
- if (!this.target) {
2216
- throw mkError("getCredentials");
2217
- }
2218
- return this.target.getCredentials();
2127
+ return this.waitForTarget.then((target) => target.getCredentials());
2219
2128
  }
2220
2129
  async getIdToken() {
2221
- if (!this.target) {
2222
- throw mkError("getIdToken");
2223
- }
2224
- 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
+ });
2225
2137
  }
2226
2138
  async signOut() {
2227
- if (!this.target) {
2228
- throw mkError("signOut");
2229
- }
2230
- await this.target.signOut();
2139
+ await this.waitForTarget.then((target) => target.signOut());
2231
2140
  location.reload();
2232
2141
  }
2233
2142
  }
@@ -2342,7 +2251,7 @@ class ApiRegistry {
2342
2251
  }
2343
2252
  }
2344
2253
 
2345
- function generateBoundRoutes(bindRoutes) {
2254
+ function resolveRouteBindings(bindRoutes) {
2346
2255
  const result = /* @__PURE__ */ new Map();
2347
2256
  if (bindRoutes) {
2348
2257
  const bind = (externalRoutes, targetRoutes) => {
@@ -2363,6 +2272,7 @@ function generateBoundRoutes(bindRoutes) {
2363
2272
  }
2364
2273
  return result;
2365
2274
  }
2275
+
2366
2276
  function getBasePath(configApi) {
2367
2277
  var _a;
2368
2278
  let { pathname } = new URL((_a = configApi.getOptionalString("app.baseUrl")) != null ? _a : "/", "http://dummy.dev");
@@ -2434,9 +2344,16 @@ class AppManager {
2434
2344
  }
2435
2345
  getProvider() {
2436
2346
  const appContext = new AppContextImpl(this);
2347
+ let routesHaveBeenValidated = false;
2437
2348
  const Provider = ({ children }) => {
2438
2349
  const appThemeApi = useMemo(() => AppThemeSelector.createWithStorage(this.themes), []);
2439
- const { routePaths, routeParents, routeObjects, featureFlags } = useMemo(() => {
2350
+ const {
2351
+ routePaths,
2352
+ routeParents,
2353
+ routeObjects,
2354
+ featureFlags,
2355
+ routeBindings
2356
+ } = useMemo(() => {
2440
2357
  const result = traverseElementTree({
2441
2358
  root: children,
2442
2359
  discoverers: [childDiscoverer, routeElementDiscoverer],
@@ -2448,12 +2365,19 @@ class AppManager {
2448
2365
  featureFlags: featureFlagCollector
2449
2366
  }
2450
2367
  });
2451
- validateRoutes(result.routePaths, result.routeParents);
2452
2368
  result.collectedPlugins.forEach((plugin) => this.plugins.add(plugin));
2453
2369
  this.verifyPlugins(this.plugins);
2454
2370
  this.getApiHolder();
2455
- return result;
2371
+ return {
2372
+ ...result,
2373
+ routeBindings: resolveRouteBindings(this.bindRoutes)
2374
+ };
2456
2375
  }, [children]);
2376
+ if (!routesHaveBeenValidated) {
2377
+ routesHaveBeenValidated = true;
2378
+ validateRouteParameters(routePaths, routeParents);
2379
+ validateRouteBindings(routeBindings, this.plugins);
2380
+ }
2457
2381
  const loadedConfig = useConfigLoader(this.configLoader, this.components, appThemeApi);
2458
2382
  const hasConfigApi = "api" in loadedConfig;
2459
2383
  if (hasConfigApi) {
@@ -2499,7 +2423,7 @@ class AppManager {
2499
2423
  routePaths,
2500
2424
  routeParents,
2501
2425
  routeObjects,
2502
- routeBindings: generateBoundRoutes(this.bindRoutes),
2426
+ routeBindings,
2503
2427
  basePath: getBasePath(loadedConfig.api)
2504
2428
  }, children))));
2505
2429
  };