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

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,21 @@
1
1
  # @backstage/frontend-app-api
2
2
 
3
+ ## 0.6.4-next.1
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
+ - Updated dependencies
9
+ - @backstage/core-app-api@1.12.4-next.0
10
+ - @backstage/frontend-plugin-api@0.6.4-next.1
11
+ - @backstage/config@1.2.0
12
+ - @backstage/core-components@0.14.4-next.0
13
+ - @backstage/core-plugin-api@1.9.1
14
+ - @backstage/errors@1.2.4
15
+ - @backstage/theme@0.5.2
16
+ - @backstage/types@1.1.1
17
+ - @backstage/version-bridge@1.0.7
18
+
3
19
  ## 0.6.4-next.0
4
20
 
5
21
  ### Patch Changes
package/dist/index.esm.js CHANGED
@@ -301,6 +301,13 @@ function isBackstageFeature(obj) {
301
301
  return false;
302
302
  }
303
303
 
304
+ function isProtectedApp() {
305
+ var _a;
306
+ const element = document.querySelector('meta[name="backstage-app-mode"]');
307
+ const appMode = (_a = element == null ? void 0 : element.getAttribute("content")) != null ? _a : "public";
308
+ return appMode === "protected";
309
+ }
310
+
304
311
  function resolveTheme(themeId, shouldPreferDark, themes) {
305
312
  if (themeId !== void 0) {
306
313
  const selectedTheme = themes.find((theme) => theme.id === themeId);
@@ -355,12 +362,134 @@ function AppThemeProvider({ children }) {
355
362
  return /* @__PURE__ */ React.createElement(appTheme.Provider, { children });
356
363
  }
357
364
 
365
+ const PLUGIN_ID = "app";
366
+ const CHANNEL_ID = `${PLUGIN_ID}-auth-cookie-expires-at`;
367
+ const MIN_BASE_DELAY_MS = 5 * 6e4;
368
+ const ERROR_BACKOFF_START = 5e3;
369
+ const ERROR_BACKOFF_FACTOR = 2;
370
+ const ERROR_BACKOFF_MAX = 5 * 6e4;
371
+ function startCookieAuthRefresh({
372
+ discoveryApi,
373
+ fetchApi,
374
+ errorApi
375
+ }) {
376
+ let stopped = false;
377
+ let timeout;
378
+ let firstError = true;
379
+ let errorBackoff = ERROR_BACKOFF_START;
380
+ const channel = "BroadcastChannel" in window ? new BroadcastChannel(CHANNEL_ID) : void 0;
381
+ const getDelay = (expiresAt) => {
382
+ const margin = (1 + 3 * Math.random()) * 6e4;
383
+ const delay = Math.max(expiresAt - Date.now(), MIN_BASE_DELAY_MS) - margin;
384
+ return delay;
385
+ };
386
+ const refresh = async () => {
387
+ try {
388
+ const baseUrl = await discoveryApi.getBaseUrl(PLUGIN_ID);
389
+ const requestUrl = `${baseUrl}/.backstage/auth/v1/cookie`;
390
+ const res = await fetchApi.fetch(requestUrl, {
391
+ credentials: "include"
392
+ });
393
+ if (!res.ok) {
394
+ throw new Error(
395
+ `Request failed with status ${res.status} ${res.statusText}, see request towards ${requestUrl} for more details`
396
+ );
397
+ }
398
+ const data = await res.json();
399
+ if (!data.expiresAt) {
400
+ throw new Error("No expiration date in response");
401
+ }
402
+ const expiresAt = Date.parse(data.expiresAt);
403
+ if (Number.isNaN(expiresAt)) {
404
+ throw new Error("Invalid expiration date in response");
405
+ }
406
+ firstError = true;
407
+ channel == null ? void 0 : channel.postMessage({
408
+ action: "COOKIE_REFRESH_SUCCESS",
409
+ payload: { expiresAt: new Date(expiresAt).toISOString() }
410
+ });
411
+ scheduleRefresh(getDelay(expiresAt));
412
+ } catch (error) {
413
+ if (firstError) {
414
+ firstError = false;
415
+ errorBackoff = ERROR_BACKOFF_START;
416
+ } else {
417
+ errorBackoff = Math.min(
418
+ ERROR_BACKOFF_MAX,
419
+ errorBackoff * ERROR_BACKOFF_FACTOR
420
+ );
421
+ console.error("Session cookie refresh failed", error);
422
+ errorApi.post(
423
+ new Error(
424
+ `Session refresh failed, see developer console for details`
425
+ )
426
+ );
427
+ }
428
+ scheduleRefresh(errorBackoff);
429
+ }
430
+ };
431
+ const onMessage = (event) => {
432
+ const { data } = event;
433
+ if (data === null || typeof data !== "object") {
434
+ return;
435
+ }
436
+ if ("action" in data && data.action === "COOKIE_REFRESH_SUCCESS") {
437
+ const expiresAt = Date.parse(data.payload.expiresAt);
438
+ if (Number.isNaN(expiresAt)) {
439
+ console.warn(
440
+ "Received invalid expiration from session refresh channel"
441
+ );
442
+ return;
443
+ }
444
+ scheduleRefresh(getDelay(expiresAt));
445
+ }
446
+ };
447
+ function scheduleRefresh(delayMs) {
448
+ if (stopped) {
449
+ return;
450
+ }
451
+ if (timeout) {
452
+ clearTimeout(timeout);
453
+ }
454
+ timeout = setTimeout(refresh, delayMs);
455
+ }
456
+ channel == null ? void 0 : channel.addEventListener("message", onMessage);
457
+ refresh();
458
+ return () => {
459
+ stopped = true;
460
+ if (timeout) {
461
+ clearTimeout(timeout);
462
+ }
463
+ channel == null ? void 0 : channel.removeEventListener("message", onMessage);
464
+ channel == null ? void 0 : channel.close();
465
+ };
466
+ }
467
+
358
468
  var __defProp$3 = Object.defineProperty;
359
469
  var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
360
470
  var __publicField$3 = (obj, key, value) => {
361
471
  __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
362
472
  return value;
363
473
  };
474
+ var __accessCheck$4 = (obj, member, msg) => {
475
+ if (!member.has(obj))
476
+ throw TypeError("Cannot " + msg);
477
+ };
478
+ var __privateGet$4 = (obj, member, getter) => {
479
+ __accessCheck$4(obj, member, "read from private field");
480
+ return getter ? getter.call(obj) : member.get(obj);
481
+ };
482
+ var __privateAdd$4 = (obj, member, value) => {
483
+ if (member.has(obj))
484
+ throw TypeError("Cannot add the same private member more than once");
485
+ member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
486
+ };
487
+ var __privateSet$4 = (obj, member, value, setter) => {
488
+ __accessCheck$4(obj, member, "write to private field");
489
+ setter ? setter.call(obj, value) : member.set(obj, value);
490
+ return value;
491
+ };
492
+ var _cookieAuthSignOut;
364
493
  function mkError(thing) {
365
494
  return new Error(
366
495
  `Tried to access IdentityApi ${thing} before app was loaded`
@@ -378,6 +507,7 @@ class AppIdentityProxy {
378
507
  __publicField$3(this, "resolveTarget", () => {
379
508
  });
380
509
  __publicField$3(this, "signOutTargetUrl", "/");
510
+ __privateAdd$4(this, _cookieAuthSignOut, void 0);
381
511
  this.waitForTarget = new Promise((resolve) => {
382
512
  this.resolveTarget = resolve;
383
513
  });
@@ -435,10 +565,29 @@ class AppIdentityProxy {
435
565
  });
436
566
  }
437
567
  async signOut() {
568
+ var _a;
438
569
  await this.waitForTarget.then((target) => target.signOut());
570
+ await ((_a = __privateGet$4(this, _cookieAuthSignOut)) == null ? void 0 : _a.call(this));
439
571
  window.location.href = this.signOutTargetUrl;
440
572
  }
573
+ enableCookieAuth(ctx) {
574
+ if (__privateGet$4(this, _cookieAuthSignOut)) {
575
+ return;
576
+ }
577
+ const stopRefresh = startCookieAuthRefresh(ctx);
578
+ __privateSet$4(this, _cookieAuthSignOut, async () => {
579
+ stopRefresh();
580
+ const appBaseUrl = await ctx.discoveryApi.getBaseUrl("app");
581
+ try {
582
+ await ctx.fetchApi.fetch(`${appBaseUrl}/.backstage/auth/v1/cookie`, {
583
+ method: "DELETE"
584
+ });
585
+ } catch {
586
+ }
587
+ });
588
+ }
441
589
  }
590
+ _cookieAuthSignOut = new WeakMap();
442
591
 
443
592
  var __defProp$2 = Object.defineProperty;
444
593
  var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -2743,6 +2892,21 @@ function createSpecializedApp(options) {
2743
2892
  ),
2744
2893
  options == null ? void 0 : options.icons
2745
2894
  );
2895
+ if (isProtectedApp()) {
2896
+ const discoveryApi = apiHolder.get(discoveryApiRef);
2897
+ const errorApi = apiHolder.get(errorApiRef);
2898
+ const fetchApi = apiHolder.get(fetchApiRef);
2899
+ if (!discoveryApi || !errorApi || !fetchApi) {
2900
+ throw new Error(
2901
+ "App is running in protected mode but missing required APIs"
2902
+ );
2903
+ }
2904
+ appIdentityProxy.enableCookieAuth({
2905
+ discoveryApi,
2906
+ errorApi,
2907
+ fetchApi
2908
+ });
2909
+ }
2746
2910
  const featureFlagApi = apiHolder.get(featureFlagsApiRef);
2747
2911
  if (featureFlagApi) {
2748
2912
  for (const feature of features) {