@backstage/frontend-app-api 0.6.4-next.0 → 0.6.4

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,40 @@
1
1
  # @backstage/frontend-app-api
2
2
 
3
+ ## 0.6.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 83f24f6: add `@backstage/no-top-level-material-ui-4-imports` lint rule
8
+ - c884b9a: The app is now aware of if it is being served from the `app-backend` with a separate public and protected bundles. When in protected mode the app will now continuously refresh the session cookie, as well as clear the cookie if the user signs out.
9
+ - 7ef7cc8: Fix duplicated subpath on routes resolved by the `useRouteRef` hook.
10
+ - abfbcfc: Updated dependency `@testing-library/react` to `^15.0.0`.
11
+ - Updated dependencies
12
+ - @backstage/core-components@0.14.4
13
+ - @backstage/core-app-api@1.12.4
14
+ - @backstage/core-plugin-api@1.9.2
15
+ - @backstage/frontend-plugin-api@0.6.4
16
+ - @backstage/theme@0.5.3
17
+ - @backstage/version-bridge@1.0.8
18
+ - @backstage/config@1.2.0
19
+ - @backstage/errors@1.2.4
20
+ - @backstage/types@1.1.1
21
+
22
+ ## 0.6.4-next.1
23
+
24
+ ### Patch Changes
25
+
26
+ - c884b9a: The app is now aware of if it is being served from the `app-backend` with a separate public and protected bundles. When in protected mode the app will now continuously refresh the session cookie, as well as clear the cookie if the user signs out.
27
+ - Updated dependencies
28
+ - @backstage/core-app-api@1.12.4-next.0
29
+ - @backstage/frontend-plugin-api@0.6.4-next.1
30
+ - @backstage/config@1.2.0
31
+ - @backstage/core-components@0.14.4-next.0
32
+ - @backstage/core-plugin-api@1.9.1
33
+ - @backstage/errors@1.2.4
34
+ - @backstage/theme@0.5.2
35
+ - @backstage/types@1.1.1
36
+ - @backstage/version-bridge@1.0.7
37
+
3
38
  ## 0.6.4-next.0
4
39
 
5
40
  ### Patch Changes
package/dist/index.esm.js CHANGED
@@ -3,7 +3,8 @@ import { ConfigReader } from '@backstage/config';
3
3
  import { createExtension, createExtensionInput, createApiExtension, createThemeExtension, createComponentExtension, createTranslationExtension, coreExtensionData, ExtensionBoundary, useComponentRef, coreComponentRefs, createNavItemExtension, createNavLogoExtension, useRouteRef, createAppRootElementExtension, createSchemaFromZod, AnalyticsContext, useAnalytics, createRouterExtension, createSignInPageExtension, createAppRootWrapperExtension, appTreeApiRef, routeResolutionApiRef, componentsApiRef, iconsApiRef } from '@backstage/frontend-plugin-api';
4
4
  import { useRoutes, BrowserRouter, useInRouterContext, MemoryRouter, matchRoutes, generatePath, useLocation } from 'react-router-dom';
5
5
  import { SidebarPage, sidebarConfig, Sidebar, SidebarDivider, useSidebarOpenState, Link, SidebarItem, Progress, ErrorPage, ErrorPanel, OAuthRequestDialog, AlertDisplay } from '@backstage/core-components';
6
- import { makeStyles, Button as Button$1 } from '@material-ui/core';
6
+ import { makeStyles as makeStyles$1 } from '@material-ui/core/styles';
7
+ import { makeStyles } from '@material-ui/core';
7
8
  import { useApi, appThemeApiRef, FeatureFlagState, createApiFactory, discoveryApiRef, configApiRef, alertApiRef, analyticsApiRef, errorApiRef, storageApiRef, fetchApiRef, identityApiRef, oauthRequestApiRef, googleAuthApiRef, microsoftAuthApiRef, githubAuthApiRef, oktaAuthApiRef, gitlabAuthApiRef, oneloginAuthApiRef, bitbucketAuthApiRef, bitbucketServerAuthApiRef, atlassianAuthApiRef, vmwareCloudAuthApiRef, featureFlagsApiRef } from '@backstage/core-plugin-api';
8
9
  import { UrlPatternDiscovery, AlertApiForwarder, NoOpAnalyticsApi, ErrorAlerter, ErrorApiForwarder, UnhandledErrorForwarder, WebStorage, createFetchApi, FetchMiddlewares, OAuthRequestManager, GoogleAuth, MicrosoftAuth, GithubAuth, OktaAuth, GitlabAuth, OneLoginAuth, BitbucketAuth, BitbucketServerAuth, AtlassianAuth, VMwareCloudAuth, ApiFactoryRegistry, AppThemeSelector, ApiResolver, ApiProvider } from '@backstage/core-app-api';
9
10
  import useObservable from 'react-use/esm/useObservable';
@@ -195,7 +196,7 @@ const LogoFull = () => {
195
196
  );
196
197
  };
197
198
 
198
- const useSidebarLogoStyles = makeStyles({
199
+ const useSidebarLogoStyles = makeStyles$1({
199
200
  root: {
200
201
  width: sidebarConfig.drawerWidthClosed,
201
202
  height: 3 * sidebarConfig.logoHeight,
@@ -301,6 +302,13 @@ function isBackstageFeature(obj) {
301
302
  return false;
302
303
  }
303
304
 
305
+ function isProtectedApp() {
306
+ var _a;
307
+ const element = document.querySelector('meta[name="backstage-app-mode"]');
308
+ const appMode = (_a = element == null ? void 0 : element.getAttribute("content")) != null ? _a : "public";
309
+ return appMode === "protected";
310
+ }
311
+
304
312
  function resolveTheme(themeId, shouldPreferDark, themes) {
305
313
  if (themeId !== void 0) {
306
314
  const selectedTheme = themes.find((theme) => theme.id === themeId);
@@ -355,12 +363,134 @@ function AppThemeProvider({ children }) {
355
363
  return /* @__PURE__ */ React.createElement(appTheme.Provider, { children });
356
364
  }
357
365
 
366
+ const PLUGIN_ID = "app";
367
+ const CHANNEL_ID = `${PLUGIN_ID}-auth-cookie-expires-at`;
368
+ const MIN_BASE_DELAY_MS = 5 * 6e4;
369
+ const ERROR_BACKOFF_START = 5e3;
370
+ const ERROR_BACKOFF_FACTOR = 2;
371
+ const ERROR_BACKOFF_MAX = 5 * 6e4;
372
+ function startCookieAuthRefresh({
373
+ discoveryApi,
374
+ fetchApi,
375
+ errorApi
376
+ }) {
377
+ let stopped = false;
378
+ let timeout;
379
+ let firstError = true;
380
+ let errorBackoff = ERROR_BACKOFF_START;
381
+ const channel = "BroadcastChannel" in window ? new BroadcastChannel(CHANNEL_ID) : void 0;
382
+ const getDelay = (expiresAt) => {
383
+ const margin = (1 + 3 * Math.random()) * 6e4;
384
+ const delay = Math.max(expiresAt - Date.now(), MIN_BASE_DELAY_MS) - margin;
385
+ return delay;
386
+ };
387
+ const refresh = async () => {
388
+ try {
389
+ const baseUrl = await discoveryApi.getBaseUrl(PLUGIN_ID);
390
+ const requestUrl = `${baseUrl}/.backstage/auth/v1/cookie`;
391
+ const res = await fetchApi.fetch(requestUrl, {
392
+ credentials: "include"
393
+ });
394
+ if (!res.ok) {
395
+ throw new Error(
396
+ `Request failed with status ${res.status} ${res.statusText}, see request towards ${requestUrl} for more details`
397
+ );
398
+ }
399
+ const data = await res.json();
400
+ if (!data.expiresAt) {
401
+ throw new Error("No expiration date in response");
402
+ }
403
+ const expiresAt = Date.parse(data.expiresAt);
404
+ if (Number.isNaN(expiresAt)) {
405
+ throw new Error("Invalid expiration date in response");
406
+ }
407
+ firstError = true;
408
+ channel == null ? void 0 : channel.postMessage({
409
+ action: "COOKIE_REFRESH_SUCCESS",
410
+ payload: { expiresAt: new Date(expiresAt).toISOString() }
411
+ });
412
+ scheduleRefresh(getDelay(expiresAt));
413
+ } catch (error) {
414
+ if (firstError) {
415
+ firstError = false;
416
+ errorBackoff = ERROR_BACKOFF_START;
417
+ } else {
418
+ errorBackoff = Math.min(
419
+ ERROR_BACKOFF_MAX,
420
+ errorBackoff * ERROR_BACKOFF_FACTOR
421
+ );
422
+ console.error("Session cookie refresh failed", error);
423
+ errorApi.post(
424
+ new Error(
425
+ `Session refresh failed, see developer console for details`
426
+ )
427
+ );
428
+ }
429
+ scheduleRefresh(errorBackoff);
430
+ }
431
+ };
432
+ const onMessage = (event) => {
433
+ const { data } = event;
434
+ if (data === null || typeof data !== "object") {
435
+ return;
436
+ }
437
+ if ("action" in data && data.action === "COOKIE_REFRESH_SUCCESS") {
438
+ const expiresAt = Date.parse(data.payload.expiresAt);
439
+ if (Number.isNaN(expiresAt)) {
440
+ console.warn(
441
+ "Received invalid expiration from session refresh channel"
442
+ );
443
+ return;
444
+ }
445
+ scheduleRefresh(getDelay(expiresAt));
446
+ }
447
+ };
448
+ function scheduleRefresh(delayMs) {
449
+ if (stopped) {
450
+ return;
451
+ }
452
+ if (timeout) {
453
+ clearTimeout(timeout);
454
+ }
455
+ timeout = setTimeout(refresh, delayMs);
456
+ }
457
+ channel == null ? void 0 : channel.addEventListener("message", onMessage);
458
+ refresh();
459
+ return () => {
460
+ stopped = true;
461
+ if (timeout) {
462
+ clearTimeout(timeout);
463
+ }
464
+ channel == null ? void 0 : channel.removeEventListener("message", onMessage);
465
+ channel == null ? void 0 : channel.close();
466
+ };
467
+ }
468
+
358
469
  var __defProp$3 = Object.defineProperty;
359
470
  var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
360
471
  var __publicField$3 = (obj, key, value) => {
361
472
  __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
362
473
  return value;
363
474
  };
475
+ var __accessCheck$4 = (obj, member, msg) => {
476
+ if (!member.has(obj))
477
+ throw TypeError("Cannot " + msg);
478
+ };
479
+ var __privateGet$4 = (obj, member, getter) => {
480
+ __accessCheck$4(obj, member, "read from private field");
481
+ return getter ? getter.call(obj) : member.get(obj);
482
+ };
483
+ var __privateAdd$4 = (obj, member, value) => {
484
+ if (member.has(obj))
485
+ throw TypeError("Cannot add the same private member more than once");
486
+ member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
487
+ };
488
+ var __privateSet$4 = (obj, member, value, setter) => {
489
+ __accessCheck$4(obj, member, "write to private field");
490
+ setter ? setter.call(obj, value) : member.set(obj, value);
491
+ return value;
492
+ };
493
+ var _cookieAuthSignOut;
364
494
  function mkError(thing) {
365
495
  return new Error(
366
496
  `Tried to access IdentityApi ${thing} before app was loaded`
@@ -378,6 +508,7 @@ class AppIdentityProxy {
378
508
  __publicField$3(this, "resolveTarget", () => {
379
509
  });
380
510
  __publicField$3(this, "signOutTargetUrl", "/");
511
+ __privateAdd$4(this, _cookieAuthSignOut, void 0);
381
512
  this.waitForTarget = new Promise((resolve) => {
382
513
  this.resolveTarget = resolve;
383
514
  });
@@ -435,10 +566,29 @@ class AppIdentityProxy {
435
566
  });
436
567
  }
437
568
  async signOut() {
569
+ var _a;
438
570
  await this.waitForTarget.then((target) => target.signOut());
571
+ await ((_a = __privateGet$4(this, _cookieAuthSignOut)) == null ? void 0 : _a.call(this));
439
572
  window.location.href = this.signOutTargetUrl;
440
573
  }
574
+ enableCookieAuth(ctx) {
575
+ if (__privateGet$4(this, _cookieAuthSignOut)) {
576
+ return;
577
+ }
578
+ const stopRefresh = startCookieAuthRefresh(ctx);
579
+ __privateSet$4(this, _cookieAuthSignOut, async () => {
580
+ stopRefresh();
581
+ const appBaseUrl = await ctx.discoveryApi.getBaseUrl("app");
582
+ try {
583
+ await ctx.fetchApi.fetch(`${appBaseUrl}/.backstage/auth/v1/cookie`, {
584
+ method: "DELETE"
585
+ });
586
+ } catch {
587
+ }
588
+ });
589
+ }
441
590
  }
591
+ _cookieAuthSignOut = new WeakMap();
442
592
 
443
593
  var __defProp$2 = Object.defineProperty;
444
594
  var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -1674,7 +1824,7 @@ class RouteResolver {
1674
1824
  return void 0;
1675
1825
  }
1676
1826
  const relativeSourceLocation = this.trimPath((_a = options == null ? void 0 : options.sourcePath) != null ? _a : "");
1677
- const basePath = this.appBasePath + resolveBasePath(
1827
+ const basePath = resolveBasePath(
1678
1828
  targetRef,
1679
1829
  relativeSourceLocation,
1680
1830
  this.routePaths,
@@ -2357,7 +2507,7 @@ const DefaultErrorBoundaryComponent = createComponentExtension({
2357
2507
  sync: () => (props) => {
2358
2508
  const { plugin, error, resetError } = props;
2359
2509
  const title = `Error in ${plugin == null ? void 0 : plugin.id}`;
2360
- return /* @__PURE__ */ React.createElement(ErrorPanel, { title, error, defaultExpanded: true }, /* @__PURE__ */ React.createElement(Button$1, { variant: "outlined", onClick: resetError }, "Retry"));
2510
+ return /* @__PURE__ */ React.createElement(ErrorPanel, { title, error, defaultExpanded: true }, /* @__PURE__ */ React.createElement(Button, { variant: "outlined", onClick: resetError }, "Retry"));
2361
2511
  }
2362
2512
  }
2363
2513
  });
@@ -2743,6 +2893,21 @@ function createSpecializedApp(options) {
2743
2893
  ),
2744
2894
  options == null ? void 0 : options.icons
2745
2895
  );
2896
+ if (isProtectedApp()) {
2897
+ const discoveryApi = apiHolder.get(discoveryApiRef);
2898
+ const errorApi = apiHolder.get(errorApiRef);
2899
+ const fetchApi = apiHolder.get(fetchApiRef);
2900
+ if (!discoveryApi || !errorApi || !fetchApi) {
2901
+ throw new Error(
2902
+ "App is running in protected mode but missing required APIs"
2903
+ );
2904
+ }
2905
+ appIdentityProxy.enableCookieAuth({
2906
+ discoveryApi,
2907
+ errorApi,
2908
+ fetchApi
2909
+ });
2910
+ }
2746
2911
  const featureFlagApi = apiHolder.get(featureFlagsApiRef);
2747
2912
  if (featureFlagApi) {
2748
2913
  for (const feature of features) {