@backstage/core-app-api 1.12.3 → 1.12.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,29 @@
1
1
  # @backstage/core-app-api
2
2
 
3
+ ## 1.12.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+ - abfbcfc: Updated dependency `@testing-library/react` to `^15.0.0`.
9
+ - cb1e3b0: Updated dependency `@testing-library/dom` to `^10.0.0`.
10
+ - Updated dependencies
11
+ - @backstage/core-plugin-api@1.9.2
12
+ - @backstage/version-bridge@1.0.8
13
+ - @backstage/config@1.2.0
14
+ - @backstage/types@1.1.1
15
+
16
+ ## 1.12.4-next.0
17
+
18
+ ### Patch Changes
19
+
20
+ - 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.
21
+ - Updated dependencies
22
+ - @backstage/config@1.2.0
23
+ - @backstage/core-plugin-api@1.9.1
24
+ - @backstage/types@1.1.1
25
+ - @backstage/version-bridge@1.0.7
26
+
3
27
  ## 1.12.3
4
28
 
5
29
  ### Patch Changes
package/dist/index.esm.js CHANGED
@@ -2,7 +2,7 @@ import React, { useContext, createContext, useEffect, useState, Children, isVali
2
2
  import PropTypes from 'prop-types';
3
3
  import { createVersionedContext, createVersionedValueMap, getOrCreateGlobalSingleton } from '@backstage/version-bridge';
4
4
  import ObservableImpl from 'zen-observable';
5
- import { SessionState, FeatureFlagState, AnalyticsContext, useAnalytics, attachComponentData, useApp, useApi, configApiRef, getComponentData, featureFlagsApiRef, appThemeApiRef, identityApiRef, useElementFilter } from '@backstage/core-plugin-api';
5
+ import { SessionState, FeatureFlagState, AnalyticsContext, useAnalytics, attachComponentData, useApp, useApi, configApiRef, getComponentData, featureFlagsApiRef, appThemeApiRef, identityApiRef, errorApiRef, fetchApiRef, discoveryApiRef, useElementFilter } from '@backstage/core-plugin-api';
6
6
  import { z } from 'zod';
7
7
  import { ConfigReader } from '@backstage/config';
8
8
  export { ConfigReader } from '@backstage/config';
@@ -2967,12 +2967,134 @@ const AppContextProvider = ({
2967
2967
  return /* @__PURE__ */ React.createElement(AppContext.Provider, { value: versionedValue, children });
2968
2968
  };
2969
2969
 
2970
+ const PLUGIN_ID = "app";
2971
+ const CHANNEL_ID = `${PLUGIN_ID}-auth-cookie-expires-at`;
2972
+ const MIN_BASE_DELAY_MS = 5 * 6e4;
2973
+ const ERROR_BACKOFF_START = 5e3;
2974
+ const ERROR_BACKOFF_FACTOR = 2;
2975
+ const ERROR_BACKOFF_MAX = 5 * 6e4;
2976
+ function startCookieAuthRefresh({
2977
+ discoveryApi,
2978
+ fetchApi,
2979
+ errorApi
2980
+ }) {
2981
+ let stopped = false;
2982
+ let timeout;
2983
+ let firstError = true;
2984
+ let errorBackoff = ERROR_BACKOFF_START;
2985
+ const channel = "BroadcastChannel" in window ? new BroadcastChannel(CHANNEL_ID) : void 0;
2986
+ const getDelay = (expiresAt) => {
2987
+ const margin = (1 + 3 * Math.random()) * 6e4;
2988
+ const delay = Math.max(expiresAt - Date.now(), MIN_BASE_DELAY_MS) - margin;
2989
+ return delay;
2990
+ };
2991
+ const refresh = async () => {
2992
+ try {
2993
+ const baseUrl = await discoveryApi.getBaseUrl(PLUGIN_ID);
2994
+ const requestUrl = `${baseUrl}/.backstage/auth/v1/cookie`;
2995
+ const res = await fetchApi.fetch(requestUrl, {
2996
+ credentials: "include"
2997
+ });
2998
+ if (!res.ok) {
2999
+ throw new Error(
3000
+ `Request failed with status ${res.status} ${res.statusText}, see request towards ${requestUrl} for more details`
3001
+ );
3002
+ }
3003
+ const data = await res.json();
3004
+ if (!data.expiresAt) {
3005
+ throw new Error("No expiration date in response");
3006
+ }
3007
+ const expiresAt = Date.parse(data.expiresAt);
3008
+ if (Number.isNaN(expiresAt)) {
3009
+ throw new Error("Invalid expiration date in response");
3010
+ }
3011
+ firstError = true;
3012
+ channel == null ? void 0 : channel.postMessage({
3013
+ action: "COOKIE_REFRESH_SUCCESS",
3014
+ payload: { expiresAt: new Date(expiresAt).toISOString() }
3015
+ });
3016
+ scheduleRefresh(getDelay(expiresAt));
3017
+ } catch (error) {
3018
+ if (firstError) {
3019
+ firstError = false;
3020
+ errorBackoff = ERROR_BACKOFF_START;
3021
+ } else {
3022
+ errorBackoff = Math.min(
3023
+ ERROR_BACKOFF_MAX,
3024
+ errorBackoff * ERROR_BACKOFF_FACTOR
3025
+ );
3026
+ console.error("Session cookie refresh failed", error);
3027
+ errorApi.post(
3028
+ new Error(
3029
+ `Session refresh failed, see developer console for details`
3030
+ )
3031
+ );
3032
+ }
3033
+ scheduleRefresh(errorBackoff);
3034
+ }
3035
+ };
3036
+ const onMessage = (event) => {
3037
+ const { data } = event;
3038
+ if (data === null || typeof data !== "object") {
3039
+ return;
3040
+ }
3041
+ if ("action" in data && data.action === "COOKIE_REFRESH_SUCCESS") {
3042
+ const expiresAt = Date.parse(data.payload.expiresAt);
3043
+ if (Number.isNaN(expiresAt)) {
3044
+ console.warn(
3045
+ "Received invalid expiration from session refresh channel"
3046
+ );
3047
+ return;
3048
+ }
3049
+ scheduleRefresh(getDelay(expiresAt));
3050
+ }
3051
+ };
3052
+ function scheduleRefresh(delayMs) {
3053
+ if (stopped) {
3054
+ return;
3055
+ }
3056
+ if (timeout) {
3057
+ clearTimeout(timeout);
3058
+ }
3059
+ timeout = setTimeout(refresh, delayMs);
3060
+ }
3061
+ channel == null ? void 0 : channel.addEventListener("message", onMessage);
3062
+ refresh();
3063
+ return () => {
3064
+ stopped = true;
3065
+ if (timeout) {
3066
+ clearTimeout(timeout);
3067
+ }
3068
+ channel == null ? void 0 : channel.removeEventListener("message", onMessage);
3069
+ channel == null ? void 0 : channel.close();
3070
+ };
3071
+ }
3072
+
2970
3073
  var __defProp$2 = Object.defineProperty;
2971
3074
  var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
2972
3075
  var __publicField$2 = (obj, key, value) => {
2973
3076
  __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
2974
3077
  return value;
2975
3078
  };
3079
+ var __accessCheck$3 = (obj, member, msg) => {
3080
+ if (!member.has(obj))
3081
+ throw TypeError("Cannot " + msg);
3082
+ };
3083
+ var __privateGet$3 = (obj, member, getter) => {
3084
+ __accessCheck$3(obj, member, "read from private field");
3085
+ return getter ? getter.call(obj) : member.get(obj);
3086
+ };
3087
+ var __privateAdd$3 = (obj, member, value) => {
3088
+ if (member.has(obj))
3089
+ throw TypeError("Cannot add the same private member more than once");
3090
+ member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
3091
+ };
3092
+ var __privateSet$3 = (obj, member, value, setter) => {
3093
+ __accessCheck$3(obj, member, "write to private field");
3094
+ setter ? setter.call(obj, value) : member.set(obj, value);
3095
+ return value;
3096
+ };
3097
+ var _cookieAuthSignOut;
2976
3098
  function mkError(thing) {
2977
3099
  return new Error(
2978
3100
  `Tried to access IdentityApi ${thing} before app was loaded`
@@ -2990,6 +3112,7 @@ class AppIdentityProxy {
2990
3112
  __publicField$2(this, "resolveTarget", () => {
2991
3113
  });
2992
3114
  __publicField$2(this, "signOutTargetUrl", "/");
3115
+ __privateAdd$3(this, _cookieAuthSignOut, void 0);
2993
3116
  this.waitForTarget = new Promise((resolve) => {
2994
3117
  this.resolveTarget = resolve;
2995
3118
  });
@@ -3047,10 +3170,29 @@ class AppIdentityProxy {
3047
3170
  });
3048
3171
  }
3049
3172
  async signOut() {
3173
+ var _a;
3050
3174
  await this.waitForTarget.then((target) => target.signOut());
3175
+ await ((_a = __privateGet$3(this, _cookieAuthSignOut)) == null ? void 0 : _a.call(this));
3051
3176
  window.location.href = this.signOutTargetUrl;
3052
3177
  }
3178
+ enableCookieAuth(ctx) {
3179
+ if (__privateGet$3(this, _cookieAuthSignOut)) {
3180
+ return;
3181
+ }
3182
+ const stopRefresh = startCookieAuthRefresh(ctx);
3183
+ __privateSet$3(this, _cookieAuthSignOut, async () => {
3184
+ stopRefresh();
3185
+ const appBaseUrl = await ctx.discoveryApi.getBaseUrl("app");
3186
+ try {
3187
+ await ctx.fetchApi.fetch(`${appBaseUrl}/.backstage/auth/v1/cookie`, {
3188
+ method: "DELETE"
3189
+ });
3190
+ } catch {
3191
+ }
3192
+ });
3193
+ }
3053
3194
  }
3195
+ _cookieAuthSignOut = new WeakMap();
3054
3196
 
3055
3197
  function resolveTheme(themeId, shouldPreferDark, themes) {
3056
3198
  if (themeId !== void 0) {
@@ -3662,6 +3804,13 @@ function overrideBaseUrlConfigs(inputConfigs) {
3662
3804
  return configs;
3663
3805
  }
3664
3806
 
3807
+ function isProtectedApp() {
3808
+ var _a;
3809
+ const element = document.querySelector('meta[name="backstage-app-mode"]');
3810
+ const appMode = (_a = element == null ? void 0 : element.getAttribute("content")) != null ? _a : "public";
3811
+ return appMode === "protected";
3812
+ }
3813
+
3665
3814
  var __defProp = Object.defineProperty;
3666
3815
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3667
3816
  var __publicField = (obj, key, value) => {
@@ -3882,7 +4031,23 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
3882
4031
  }
3883
4032
  }
3884
4033
  const { ThemeProvider = AppThemeProvider, Progress } = this.components;
3885
- return /* @__PURE__ */ React.createElement(ApiProvider, { apis: this.getApiHolder() }, /* @__PURE__ */ React.createElement(AppContextProvider, { appContext }, /* @__PURE__ */ React.createElement(ThemeProvider, null, /* @__PURE__ */ React.createElement(
4034
+ const apis = this.getApiHolder();
4035
+ if (isProtectedApp()) {
4036
+ const errorApi = apis.get(errorApiRef);
4037
+ const fetchApi = apis.get(fetchApiRef);
4038
+ const discoveryApi = apis.get(discoveryApiRef);
4039
+ if (!errorApi || !fetchApi || !discoveryApi) {
4040
+ throw new Error(
4041
+ "App is running in protected mode but missing required APIs"
4042
+ );
4043
+ }
4044
+ this.appIdentityProxy.enableCookieAuth({
4045
+ errorApi,
4046
+ fetchApi,
4047
+ discoveryApi
4048
+ });
4049
+ }
4050
+ return /* @__PURE__ */ React.createElement(ApiProvider, { apis }, /* @__PURE__ */ React.createElement(AppContextProvider, { appContext }, /* @__PURE__ */ React.createElement(ThemeProvider, null, /* @__PURE__ */ React.createElement(
3886
4051
  RoutingProvider,
3887
4052
  {
3888
4053
  routePaths: routing.paths,