@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 +24 -0
- package/dist/index.esm.js +167 -2
- package/dist/index.esm.js.map +1 -1
- package/package.json +7 -7
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
|
-
|
|
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,
|