@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 +35 -0
- package/dist/index.esm.js +169 -4
- package/dist/index.esm.js.map +1 -1
- package/package.json +10 -10
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
|
|
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 =
|
|
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
|
|
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) {
|