Repository: ForgeRock/forgerock-javascript-sdk
Pull request #535: feat(javascript-sdk): ping one signout, flag for iframe skip, remove session call
Author: login: cerebrl
Labels: name: sdk
name: test
Created: 2025-04-10T02:46:02Z | Merged: 2025-04-16T22:37:31Z

# JIRA Ticket

[SDKS-3757](https://pingidentity.atlassian.net/browse/SDKS-3757) & [SDKS-3866](https://pingidentity.atlassian.net/browse/SDKS-3866)

## Description

This adds the new PingOne `/idpSignoff` endpount for ID Token only signoff without redirect. 

<!--
Did you add a changeset?
-->



Diff:
diff --git a/.changeset/rude-sloths-pay.md b/.changeset/rude-sloths-pay.md
new file mode 100644
index 000000000..61d87aaf2
--- /dev/null
+++ b/.changeset/rude-sloths-pay.md
@@ -0,0 +1,5 @@
+---
+'@forgerock/javascript-sdk': minor
+---
+
+Add new PingOne signoff, remove unneeded /session call, add flag for iframe
diff --git a/.gitignore b/.gitignore
index cd9444f8d..adc4008d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 */DS_Store
 **/*.DS_Store
 .pnpm-store/*
+
 # Generated code
 tmp/
 e2e/**/dist
@@ -30,9 +31,10 @@ packages/javascript-sdk/lib/
 .swc
 .vite
 .env.serve.development
+package-lock.json
 
 # Certificates
-# *.pem
+*.pem
 
 # IDEs
 .vscode
@@ -83,4 +85,4 @@ outputs/*
 
 e2e/mock-api-v2/html/*
 
-vitest.config.*.timestamp*
\ No newline at end of file
+vitest.config.*.timestamp*
diff --git a/e2e/autoscript-apps/index.html b/e2e/autoscript-apps/index.html
index 1a85e6f0e..03dd697a9 100644
--- a/e2e/autoscript-apps/index.html
+++ b/e2e/autoscript-apps/index.html
@@ -26,14 +26,18 @@
     <a href="./src/authn-basic/index.html">AuthN: Basic</a><br />
     <a href="./src/authn-basic-self-service/">AuthN: Self Service</a><br />
     <a href="./src/authn-central-login/index.html">AuthN: Central Login</a><br />
+    <a href="./src/authn-central-login-no-iframe/index.html">AuthN: Central Login, no iframe</a
+    ><br />
     <a href="./src/authn-central-logout/index.html">AuthN: Central Logout Ping</a><br />
+    <a href="./src/authn-central-logout-wellknown/index.html"
+      >AuthN: Central Logout with Wellknown</a
+    ><br />
     <a href="./src/authn-device-profile/index.html">AuthN: Device Profile</a><br />
     <a href="./src/authn-protect/index.html">AuthN: Ping Protect</a><br />
     <a href="./src/authn-email-suspend/index.html">AuthN: Email Suspend</a><br />
     <a href="./src/authn-recaptcha-enterprise/index.html">AuthN: Recaptcha Enterprise</a><br />
     <a href="./src/authn-no-session/index.html">AuthN: No Session</a><br />
     <a href="./src/authn-oauth/index.html">AuthN: OAuth</a><br />
-    <a href="./src/authn-wellknown/index.html">AuthN: WellKnown</a><br />
     <a href="./src/authn-platform/index.html">AuthN: Platform Login</a><br />
     <a href="./src/authn-second-factor/index.html">AuthN: Second Factor</a><br />
     <a href="./src/authn-saml/index.html">AuthN: SAML</a><br />
diff --git a/e2e/autoscript-apps/src/authn-central-login-no-iframe/autoscript.ts b/e2e/autoscript-apps/src/authn-central-login-no-iframe/autoscript.ts
new file mode 100644
index 000000000..45a3c1677
--- /dev/null
+++ b/e2e/autoscript-apps/src/authn-central-login-no-iframe/autoscript.ts
@@ -0,0 +1,145 @@
+/*
+ * @forgerock/javascript-sdk
+ *
+ * autoscript.ts
+ *
+ * Copyright (c) 2020 ForgeRock. All rights reserved.
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+// @ts-nocheck
+import * as forgerock from '@forgerock/javascript-sdk';
+import { delay as rxDelay, map, mergeMap } from 'rxjs/operators';
+import { from } from 'rxjs';
+
+function autoscript() {
+  const delay = 0;
+
+  const url = new URL(window.location.href);
+  const amUrl = url.searchParams.get('amUrl') || 'http://localhost:9443/am';
+  const preAuthenticated = url.searchParams.get('preAuthenticated') || 'false';
+  const code = url.searchParams.get('code') || '';
+  const clientId = url.searchParams.get('clientId');
+  const client_id = url.searchParams.get('client_id');
+  const error = url.searchParams.get('error_description') || false;
+  const realmPath = url.searchParams.get('realmPath') || 'root';
+  const scope = url.searchParams.get('scope') || 'openid profile me.read';
+  const state = url.searchParams.get('state') || '';
+  const acr_values = url.searchParams.get('acr') || 'skipBackgroundRequest';
+  // in central login we use an auth query param for the return of our mock 401 request
+  // this is to prevent the evaluation of the page before we have technically authenticated
+  const auth = url.searchParams.get('auth') || false;
+
+  let tokenStore = url.searchParams.get('tokenStore') || 'localStorage';
+
+  // Support full redirects by setting storage, rather than rely purely on URL
+  if (!localStorage.getItem('tokenStore')) {
+    localStorage.setItem('tokenStore', tokenStore);
+  } else {
+    tokenStore = localStorage.getItem('tokenStore');
+  }
+
+  console.log('Configure the SDK');
+  forgerock.Config.set({
+    clientId: clientId || client_id || 'CentralLoginOAuthClient',
+    realmPath,
+    redirectUri: `${url.origin}/src/${
+      preAuthenticated === 'false' ? 'authn-central-login' : '_callback'
+    }/`,
+    scope,
+    serverConfig: {
+      baseUrl: amUrl,
+    },
+    tokenStore,
+  });
+
+  if (!code && !state) {
+    try {
+      forgerock.SessionManager.logout();
+    } catch (err) {
+      // Do nothing
+    }
+  }
+
+  console.log('Initiate first step with `undefined`');
+
+  // Wrapping in setTimeout to give the test time to bind listener to console.log
+  setTimeout(() => {
+    from([1])
+      .pipe(
+        map(() => {
+          if (preAuthenticated === 'true') {
+            console.log('Set mock cookie to represent existing session');
+            document.cookie = 'iPlanetDirectoryPro=abcd1234; domain=localhost; path=/';
+            if (code && state) {
+              window.sessionStorage.setItem(
+                `FR-SDK-${clientId}`,
+                JSON.stringify({ responseType: 'code', state, verifier: '1234' }),
+              );
+            }
+          }
+          return;
+        }),
+        rxDelay(delay),
+        mergeMap((step) => {
+          let tokens;
+          // detect when in iframe, throw as error if so
+          if (window.self !== window.top) {
+            throw new Error('Loaded_In_Iframe');
+          } else if (code && state) {
+            tokens = forgerock.TokenManager.getTokens({
+              query: { code, state, acr_values },
+            });
+          } else {
+            tokens = forgerock.TokenManager.getTokens({
+              skipBackgroundRequest: true,
+              login: 'redirect',
+              query: { acr_values },
+            });
+          }
+          return tokens;
+        }),
+        map((tokens) => {
+          if (tokens.accessToken) {
+            console.log('OAuth authorization successful');
+            document.body.innerHTML = '<p class="Logged_In">Login successful</p>';
+          } else {
+            throw new Error('Session_Error');
+          }
+        }),
+        rxDelay(delay),
+        mergeMap(() => {
+          console.log('Remove cookie');
+          document.cookie = '';
+          console.log('Initiate logout');
+          return forgerock.FRUser.logout();
+        }),
+      )
+      .subscribe({
+        error: (err) => {
+          /*
+           * We added this because Playwright was too fast for the dom element.
+           * When we make a request to central login we have to force a 401 page to mimick the real life scenario of the page being requested
+           * If we do this, we append a query param of auth to make sure we don't complete the flow until we are redirected from that page
+           * By saying we have !auth query param value, we are essentially mimicking the idea that we are waiting for the central login redirect
+           * to complete the redirect.
+           */
+          if (!auth) {
+            return;
+          }
+          console.log(`Error: ${err.message}`);
+          document.body.innerHTML = `<p class="Test_Complete">${err.message}</p>`;
+          localStorage.clear();
+        },
+        complete: () => {
+          console.log('Test script complete');
+          document.body.innerHTML = `<p class="Test_Complete">Test script complete</p>`;
+          history.replaceState(null, null, window.location.origin + window.location.pathname);
+          localStorage.clear();
+        },
+      });
+  }, 250);
+}
+
+autoscript();
+export default autoscript;
diff --git a/e2e/autoscript-apps/src/authn-wellknown/index.html b/e2e/autoscript-apps/src/authn-central-login-no-iframe/index.html
similarity index 83%
rename from e2e/autoscript-apps/src/authn-wellknown/index.html
rename to e2e/autoscript-apps/src/authn-central-login-no-iframe/index.html
index e7419ea4f..016c1758f 100644
--- a/e2e/autoscript-apps/src/authn-wellknown/index.html
+++ b/e2e/autoscript-apps/src/authn-central-login-no-iframe/index.html
@@ -28,8 +28,6 @@
   </head>
 
   <body>
-    <!-- script src="/_polyfills/fast-text-encoder.js"></script -->
-
-    <script src="autoscript.ts" type="module"></script>
+    <script type="module" src="./autoscript.ts"></script>
   </body>
 </html>
diff --git a/e2e/autoscript-apps/src/authn-central-login-wellknown/autoscript.ts b/e2e/autoscript-apps/src/authn-central-login-wellknown/autoscript.ts
index ed26ebbd0..848fa2a78 100644
--- a/e2e/autoscript-apps/src/authn-central-login-wellknown/autoscript.ts
+++ b/e2e/autoscript-apps/src/authn-central-login-wellknown/autoscript.ts
@@ -16,49 +16,50 @@ async function autoscript() {
   const delay = 0;
 
   const url = new URL(window.location.href);
+  const preAuthenticated = url.searchParams.get('preAuthenticated') || 'false';
   const code = url.searchParams.get('code') || '';
-  const error = url.searchParams.get('error') || '';
+  const clientId = url.searchParams.get('clientId');
+  const client_id = url.searchParams.get('client_id');
+  const error = url.searchParams.get('error_description') || false;
+  const realmPath = url.searchParams.get('realmPath') || 'root';
+  const scope = url.searchParams.get('scope') || 'openid profile me.read';
   const state = url.searchParams.get('state') || '';
+  const acr_values = url.searchParams.get('acr') || 'SpecificTree';
   // in central login we use an auth query param for the return of our mock 401 request
   // this is to prevent the evaluation of the page before we have technically authenticated
   const auth = url.searchParams.get('auth') || false;
-  const acr_values = url.searchParams.get('acr') || 'SpecificTree';
+  let wellknown =
+    url.searchParams.get('wellknown') || 'http://localhost:9443/am/.well-known/oidc-configuration';
 
-  let clientId = url.searchParams.get('clientId') || 'CentralLoginOAuthClient';
-  let realmPath = url.searchParams.get('realmPath') || 'root';
-  // The `revoke` scope is required for PingOne support
-  let scope = url.searchParams.get('scope') || 'openid profile me.read revoke';
-  let wellKnownUrl =
-    url.searchParams.get('wellKnownUrl') ||
-    'http://localhost:9443/am/.well-known/oidc-configuration';
+  let tokenStore = url.searchParams.get('tokenStore') || 'localStorage';
 
-  console.log('Configure the SDK');
-
-  if (wellKnownUrl) {
-    localStorage.setItem('wellknown', wellKnownUrl);
-    localStorage.setItem('clientId', clientId);
-    localStorage.setItem('realmPath', realmPath);
-    localStorage.setItem('scope', scope);
+  // Support full redirects by setting storage, rather than rely purely on URL
+  if (!localStorage.getItem('tokenStore')) {
+    localStorage.setItem('tokenStore', tokenStore);
   } else {
-    wellKnownUrl = localStorage.getItem('wellknown');
-    clientId = localStorage.getItem('clientId');
-    realmPath = localStorage.getItem('realmPath');
-    scope = localStorage.getItem('scope');
+    tokenStore = localStorage.getItem('tokenStore');
   }
-  await forgerock.Config.setAsync({
-    clientId,
+
+  console.log('Configure the SDK');
+  forgerock.Config.setAsync({
+    clientId: clientId || client_id || 'CentralLoginOAuthClient',
     realmPath,
-    redirectUri: `${url.origin}/src/authn-central-login-wellknown/`,
+    redirectUri: `${url.origin}/src/${
+      preAuthenticated === 'false' ? 'authn-central-login' : '_callback'
+    }/`,
     scope,
     serverConfig: {
-      wellknown: wellKnownUrl,
+      wellknown,
     },
+    tokenStore,
   });
 
-  try {
-    forgerock.SessionManager.logout();
-  } catch (err) {
-    // Do nothing
+  if (!code && !state) {
+    try {
+      forgerock.SessionManager.logout();
+    } catch (err) {
+      // Do nothing
+    }
   }
 
   console.log('Initiate first step with `undefined`');
@@ -67,15 +68,29 @@ async function autoscript() {
   setTimeout(() => {
     from([1])
       .pipe(
-        mergeMap(() => {
+        map(() => {
+          if (preAuthenticated === 'true') {
+            console.log('Set mock cookie to represent existing session');
+            document.cookie = 'iPlanetDirectoryPro=abcd1234; domain=localhost; path=/';
+            if (code && state) {
+              window.sessionStorage.setItem(
+                `FR-SDK-authflow-${clientId}`,
+                JSON.stringify({ responseType: 'code', state, verifier: '1234' }),
+              );
+            }
+          }
+          return;
+        }),
+        rxDelay(delay),
+        mergeMap((step) => {
           let tokens;
-          // detect when in iframe as to not call `/authorize` needlessly
-          if (window.self !== window.top) {
+          if (error) {
+            // Do nothing
             return;
           } else if (code && state) {
             tokens = forgerock.TokenManager.getTokens({
               login: 'redirect',
-              query: { code, state },
+              query: { code, state, acr_values },
             });
           } else {
             tokens = forgerock.TokenManager.getTokens({
@@ -98,7 +113,6 @@ async function autoscript() {
           console.log('Remove cookie');
           document.cookie = '';
           console.log('Initiate logout');
-          // You have to allow specific origins to CORS for OAuth client
           return forgerock.FRUser.logout();
         }),
       )
@@ -116,14 +130,12 @@ async function autoscript() {
           }
           console.log(`Error: ${err.message}`);
           document.body.innerHTML = `<p class="Test_Complete">${err.message}</p>`;
+          localStorage.clear();
         },
         complete: () => {
           console.log('Test script complete');
           document.body.innerHTML = `<p class="Test_Complete">Test script complete</p>`;
-          localStorage.removeItem('wellknown');
-          localStorage.removeItem('clientId');
-          localStorage.removeItem('realmPath');
-          localStorage.removeItem('scope');
+          localStorage.clear();
         },
       });
   }, 250);
diff --git a/e2e/autoscript-apps/src/authn-central-logout/autoscript.ts b/e2e/autoscript-apps/src/authn-central-logout/autoscript.ts
index e69a89cd8..27a80d782 100644
--- a/e2e/autoscript-apps/src/authn-central-logout/autoscript.ts
+++ b/e2e/autoscript-apps/src/authn-central-logout/autoscript.ts
@@ -42,9 +42,7 @@ async function autoscript() {
 
   const logout = async () => {
     try {
-      await forgerock.FRUser.logout({
-        logoutRedirectUri: `${window.location.origin}${window.location.pathname}`,
-      });
+      await forgerock.FRUser.logout();
     } catch (error) {
       console.error(error);
     }
@@ -67,7 +65,7 @@ async function autoscript() {
      * Passing no arguments or a key-value of `login: 'embedded'` means
      * the app handles authentication locally.
      */
-    await forgerock.TokenManager.getTokens({ login: 'redirect' });
+    await forgerock.TokenManager.getTokens({ login: 'redirect', skipBackgroundRequest: true });
     const user = await forgerock.UserManager.getCurrentUser();
     showUser(user);
   });
@@ -80,10 +78,9 @@ async function autoscript() {
   await forgerock.Config.setAsync({
     clientId: '724ec718-c41c-4d51-98b0-84a583f450f9', // e.g. 'ForgeRockSDKClient'
     redirectUri: `${window.location.origin}${window.location.pathname}`, // Redirect back to your app, e.g. 'https://sdkapp.example.com:8443'
-    scope: 'openid profile email name revoke', // e.g. 'openid profile email address phone me.read'
+    scope: 'openid profile email revoke', // e.g. 'openid profile email address phone me.read'
     serverConfig: {
-      wellknown:
-        'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
+      wellknown: 'https://pingone.petrov.ca/as/.well-known/openid-configuration',
     },
     realmPath: '', // e.g. 'alpha' or 'root'
   });
diff --git a/e2e/autoscript-apps/src/authn-central-logout/index.html b/e2e/autoscript-apps/src/authn-central-logout/index.html
index 45f767ba1..f5fee8878 100644
--- a/e2e/autoscript-apps/src/authn-central-logout/index.html
+++ b/e2e/autoscript-apps/src/authn-central-logout/index.html
@@ -9,6 +9,14 @@
       rel="stylesheet"
       href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
     />
+    <style>
+      #Error {
+        display: none;
+      }
+      #Fatal {
+        display: none;
+      }
+    </style>
   </head>
   <script type="module" src="autoscript.ts"></script>
   <body>
diff --git a/e2e/autoscript-apps/src/authn-oauth/autoscript.ts b/e2e/autoscript-apps/src/authn-oauth/autoscript.ts
index 7f51c1d5e..3808c6f03 100644
--- a/e2e/autoscript-apps/src/authn-oauth/autoscript.ts
+++ b/e2e/autoscript-apps/src/authn-oauth/autoscript.ts
@@ -44,7 +44,7 @@ function autoscript() {
         next();
       },
     ],
-    redirectUri: `${url.origin}/_callback/`,
+    redirectUri: `${url.origin}/src/_callback/`,
     realmPath,
     scope,
     tree,
diff --git a/e2e/autoscript-apps/src/authn-wellknown/UsernamePassword.png b/e2e/autoscript-apps/src/authn-wellknown/UsernamePassword.png
deleted file mode 100644
index 33e10da37..000000000
Binary files a/e2e/autoscript-apps/src/authn-wellknown/UsernamePassword.png and /dev/null differ
diff --git a/e2e/autoscript-apps/src/authn-wellknown/autoscript.ts b/e2e/autoscript-apps/src/authn-wellknown/autoscript.ts
deleted file mode 100644
index d8f8bd3dd..000000000
--- a/e2e/autoscript-apps/src/authn-wellknown/autoscript.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * @forgerock/javascript-sdk
- *
- * autoscript.ts
- *
- * Copyright (c) 2020 ForgeRock. All rights reserved.
- * This software may be modified and distributed under the terms
- * of the MIT license. See the LICENSE file for details.
- */
-// @ts-nocheck
-import * as forgerock from '@forgerock/javascript-sdk';
-import { delay as rxDelay, map, mergeMap } from 'rxjs/operators';
-import { from } from 'rxjs';
-
-async function autoscript() {
-  const delay = 0;
-  const tokenExpiredDelay = 2000;
-
-  const url = new URL(window.location.href);
-  const clientId = url.searchParams.get('clientId') || 'WebOAuthClient';
-  const realmPath = url.searchParams.get('realmPath') || 'root';
-  const scope = url.searchParams.get('scope') || 'openid profile me.read';
-  const un = url.searchParams.get('un') || 'sdkuser';
-  const pw = url.searchParams.get('pw') || 'password';
-  const tree = url.searchParams.get('tree') || 'UsernamePassword';
-  const oauthThreshold = url.searchParams.get('oauthThreshold');
-  const resourceOrigin = url.searchParams.get('resourceOrigin') || 'http://localhost:9443/resource';
-  const igUrl = url.searchParams.get('igUrl'); // only use when testing against IG on different host
-  const wellKnownUrl =
-    url.searchParams.get('wellKnownUrl') ||
-    'http://localhost:9443/am/.well-known/oidc-configuration';
-
-  console.log('Configure the SDK');
-  await forgerock.Config.setAsync({
-    clientId,
-    middleware: [
-      (req, action, next) => {
-        switch (action.type) {
-          case 'AUTHORIZE':
-            console.log('Calling authorize endpoint');
-            break;
-          case 'EXCHANGE_TOKEN':
-            console.log('Calling access token exchange endpoint');
-            break;
-        }
-        next();
-      },
-    ],
-    redirectUri: `${url.origin}/_callback/`,
-    realmPath,
-    scope,
-    tree,
-    serverConfig: {
-      wellknown: wellKnownUrl,
-    },
-    oauthThreshold: oauthThreshold,
-  });
-
-  try {
-    forgerock.SessionManager.logout();
-  } catch (err) {
-    // Do nothing
-  }
-
-  console.log('Initiate first step with `undefined`');
-  from(forgerock.FRAuth.next())
-    .pipe(
-      mergeMap((step) => {
-        console.log('Set values on auth tree callbacks');
-        step.getCallbackOfType('NameCallback').setName(un);
-        step.getCallbackOfType('PasswordCallback').setPassword(pw);
-        return forgerock.FRAuth.next(step);
-      }),
-      rxDelay(delay),
-      mergeMap((step) => {
-        if (step.payload.status === 401) {
-          throw new Error('Auth_Error');
-        }
-        console.log('Auth tree successfully completed');
-        console.log('Get OAuth tokens');
-        const tokens = forgerock.TokenManager.getTokens();
-        return tokens;
-      }),
-      rxDelay(delay),
-      map((tokens) => {
-        if (tokens.accessToken) {
-          console.log('OAuth login successful');
-          document.body.innerHTML = '<p class="Logged_In">Login successful</p>';
-        } else {
-          throw new Error('Session_Error');
-        }
-        return tokens;
-      }),
-      rxDelay(delay),
-      mergeMap(
-        (tokens) => {
-          console.log('Get user info from OAuth endpoint');
-          const user = forgerock.UserManager.getCurrentUser();
-          return user;
-        },
-        (tokens, user) => {
-          console.log(`User's given name: ${user.family_name}`);
-          return tokens;
-        },
-      ),
-      rxDelay(delay),
-      mergeMap(
-        (tokens) => {
-          console.log('Force renew OAuth tokens');
-          return forgerock.TokenManager.getTokens({ forceRenew: true });
-        },
-        (oldTokens, newTokens) => {
-          if (oldTokens.accessToken !== newTokens.accessToken) {
-            console.log('New OAuth tokens retrieved');
-          } else {
-            throw new Error('Force_Renew_Error');
-          }
-          return newTokens;
-        },
-      ),
-      rxDelay(delay),
-      mergeMap(() => {
-        console.log('Initiate logout');
-        return forgerock.FRUser.logout();
-      }),
-      rxDelay(delay),
-      mergeMap(
-        (step) => {
-          return forgerock.TokenStorage.get();
-        },
-        (step, tokens) => {
-          if (!tokens) {
-            console.log('Logout successful');
-            document.body.innerHTML = '<p class="Logged_Out">Logout successful</p>';
-          } else {
-            throw new Error('Logout_Error');
-          }
-          return step;
-        },
-      ),
-      rxDelay(delay),
-    )
-    .subscribe({
-      error: (err) => {
-        console.log(`Error: ${err.message}`);
-        document.body.innerHTML = `<p class="Test_Complete">${err.message}</p>`;
-      },
-      complete: () => {
-        console.log('Test script complete');
-        document.body.innerHTML = `<p class="Test_Complete">Test script complete</p>`;
-      },
-    });
-}
-
-autoscript();
-export default autoscript;
diff --git a/e2e/autoscript-apps/vite.config.ts b/e2e/autoscript-apps/vite.config.ts
index f9d0a6219..54e2ae8a8 100644
--- a/e2e/autoscript-apps/vite.config.ts
+++ b/e2e/autoscript-apps/vite.config.ts
@@ -10,7 +10,6 @@ const pages = [
   'authn-email-suspend',
   'authn-no-session',
   'authn-oauth',
-  'authn-wellknown',
   'authn-platform',
   'authn-saml',
   'authn-second-factor',
diff --git a/e2e/autoscript-suites/src/env.config.ts b/e2e/autoscript-suites/src/env.config.ts
index 37d025f2d..f93e4a9d6 100644
--- a/e2e/autoscript-suites/src/env.config.ts
+++ b/e2e/autoscript-suites/src/env.config.ts
@@ -64,6 +64,7 @@ export const APP_PORT = ports.app;
 export const AM_PORT = amPort;
 export const MOCK_PORT = ports.mock;
 
+export const ACR = '';
 export const AM_URL = `${amUrl}:${amPort}${paths.am}`;
 export const BASE_URL = `${origins.app}:${ports.app}`;
 export const CLIENT_ID = oauth.client;
diff --git a/e2e/autoscript-suites/src/suites/authn-central-login-no-iframe.test.ts b/e2e/autoscript-suites/src/suites/authn-central-login-no-iframe.test.ts
new file mode 100644
index 000000000..2a0f7beba
--- /dev/null
+++ b/e2e/autoscript-suites/src/suites/authn-central-login-no-iframe.test.ts
@@ -0,0 +1,36 @@
+/*
+ * @forgerock/javascript-sdk
+ *
+ * authn-central-login.test.ts
+ *
+ * Copyright (c) 2020 ForgeRock. All rights reserved.
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+
+import { test, expect } from '@playwright/test';
+import { setupAndGo } from '../utilities/setup-and-go.js';
+
+test.describe('Test OAuth login flow without iframe', () => {
+  // eslint-disable-next-line
+  test(`should use full redirect to request auth code, then token exchange`, async ({
+    page,
+    browserName,
+  }) => {
+    const { messageArray, networkArray } = await setupAndGo(
+      page,
+      browserName,
+      'authn-central-login-no-iframe/',
+      { acr: 'skipBackgroundRequest' },
+    );
+
+    // Test assertions
+    // Test log messages
+    expect(messageArray.includes('OAuth authorization successful')).toBe(true);
+    expect(messageArray.includes('Test script complete')).toBe(true);
+    // Test network requests
+    // Authorize endpoint should use iframe, which is type "document"
+    expect(networkArray.includes('/am/oauth2/realms/root/authorize, document')).toBe(true);
+    expect(networkArray.includes('/am/oauth2/realms/root/access_token, fetch')).toBe(true);
+  });
+});
diff --git a/e2e/autoscript-suites/src/suites/authn-central-login-wellknown.test.ts b/e2e/autoscript-suites/src/suites/authn-central-login-wellknown.test.ts
index 7ff1eecdc..9bc3ab39d 100644
--- a/e2e/autoscript-suites/src/suites/authn-central-login-wellknown.test.ts
+++ b/e2e/autoscript-suites/src/suites/authn-central-login-wellknown.test.ts
@@ -12,8 +12,7 @@ import { test, expect } from '@playwright/test';
 import { setupAndGo } from '../utilities/setup-and-go';
 
 test.describe('Test OAuth login flow with .wellknown config', () => {
-  // eslint-disable-next-line
-  test(`should use full redirect to request auth code, then token exchange`, async ({
+  test(`should use full redirect to FR for request auth code, then token exchange`, async ({
     page,
     browserName,
   }) => {
@@ -22,6 +21,9 @@ test.describe('Test OAuth login flow with .wellknown config', () => {
       browserName,
       'authn-central-login-wellknown/',
       {
+        preAuthenticated: 'true',
+        code: 'foo',
+        state: 'abc123',
         clientId: 'CentralLoginOAuthClient',
         wellknown: 'http://localhost:9443/am/.well-known/oidc-configuration',
       },
@@ -31,9 +33,36 @@ test.describe('Test OAuth login flow with .wellknown config', () => {
     // Test log messages
     expect(messageArray.includes('OAuth authorization successful')).toBe(true);
     expect(messageArray.includes('Test script complete')).toBe(true);
+
+    // Test network requests
+    expect(networkArray.includes('/am/json/realms/root/sessions, fetch')).toBe(true);
+    expect(networkArray.includes('/am/oauth2/realms/root/connect/endSession, fetch')).toBe(true);
+  });
+
+  test(`should use full redirect to PingOne with new Wellknown for request auth code, then token exchange`, async ({
+    page,
+    browserName,
+  }) => {
+    const { messageArray, networkArray } = await setupAndGo(
+      page,
+      browserName,
+      'authn-central-login-wellknown/',
+      {
+        preAuthenticated: 'true',
+        code: 'foo',
+        state: 'abc123',
+        clientId: 'CentralLoginOAuthClient',
+        wellknown: 'http://localhost:9443/as/.well-known/new-oidc-configuration',
+      },
+    );
+
+    // Test assertions
+    // Test log messages
+    expect(messageArray.includes('OAuth authorization successful')).toBe(true);
+    expect(messageArray.includes('Test script complete')).toBe(true);
+
     // Test network requests
-    // Authorize endpoint should use iframe, which is type "document"
-    expect(networkArray.includes('/am/oauth2/realms/root/authorize, document')).toBe(true);
-    expect(networkArray.includes('/am/oauth2/realms/root/access_token, fetch')).toBe(true);
+    expect(networkArray.includes('/am/json/realms/root/sessions, fetch')).toBe(false);
+    expect(networkArray.includes('/am/oauth2/realms/root/connect/idpEndSession, fetch')).toBe(true);
   });
 });
diff --git a/e2e/autoscript-suites/src/suites/authn-central-logout.test.ts b/e2e/autoscript-suites/src/suites/authn-central-logout.test.ts
index 10037e966..a8af984fc 100644
--- a/e2e/autoscript-suites/src/suites/authn-central-logout.test.ts
+++ b/e2e/autoscript-suites/src/suites/authn-central-logout.test.ts
@@ -9,13 +9,11 @@ test('should login and logout with pingone', async ({ page }) => {
   await expect(btn).toBeVisible();
   await btn.click({ delay: 1000 });
   await page.waitForURL(/ping/);
-  await page.getByRole('textbox', { name: 'Username' }).click();
-  await page.getByPlaceholder('Username').fill('sdk.user');
-  await page.getByRole('textbox', { name: 'Password' }).click();
-  await page.getByRole('textbox', { name: 'Password' }).fill('XZY8gqn3gau*jmv1hwg');
+  await page.getByPlaceholder('Username').fill('reactdavinci@user.com');
+  await page.getByRole('textbox', { name: 'Password' }).fill('abc123!ABC');
   await page.getByRole('button', { name: 'Sign On' }).click();
 
-  await expect(page.getByText('preferred_username')).toContainText('sdk.user');
+  await expect(page.getByText('preferred_username')).toContainText('reactdavinci@user.com');
   await page.getByRole('button', { name: 'Sign Out' }).click({ clickCount: 1, delay: 300 });
   await page.getByRole('button', { name: 'Login' }).click({ delay: 300 });
   await page.waitForRequest(/pingone/);
diff --git a/e2e/autoscript-suites/src/utilities/setup-and-go.ts b/e2e/autoscript-suites/src/utilities/setup-and-go.ts
index a451302fa..441dbf4a5 100644
--- a/e2e/autoscript-suites/src/utilities/setup-and-go.ts
+++ b/e2e/autoscript-suites/src/utilities/setup-and-go.ts
@@ -9,13 +9,23 @@
  */
 
 import type { Page } from '@playwright/test';
-import { AM_URL, BASE_URL, CLIENT_ID, RESOURCE_URL, SCOPE, REALM_PATH, USERS } from '../env.config';
+import {
+  ACR,
+  AM_URL,
+  BASE_URL,
+  CLIENT_ID,
+  RESOURCE_URL,
+  SCOPE,
+  REALM_PATH,
+  USERS,
+} from '../env.config';
 
 export async function setupAndGo(
   page: Page,
   browserType: string,
   path: string,
   config?: {
+    acr?: string;
     allowGeo?: boolean;
     amUrl?: string;
     pauseBehaviorData?: string; // for protect behavioral data collection
@@ -52,6 +62,7 @@ export async function setupAndGo(
   // If anything fails, ensure we close the browser to end the process
   const url = new URL(`${BASE_URL}/src/${path}`);
 
+  url.searchParams.set('acr', (config && config.acr) || ACR);
   url.searchParams.set('amUrl', (config && config.amUrl) || AM_URL);
   url.searchParams.set('pauseBehaviorData', (config && config.pauseBehaviorData) || '');
   url.searchParams.set('clientId', (config && config.clientId) || CLIENT_ID);
diff --git a/e2e/mock-api/src/app/constants.js b/e2e/mock-api/src/app/constants.js
index e9bba07ed..31e8c2354 100644
--- a/e2e/mock-api/src/app/constants.js
+++ b/e2e/mock-api/src/app/constants.js
@@ -36,6 +36,7 @@ export const authPaths = {
   endSession: [
     '/am/auth/endSession',
     '/am/oauth2/realms/root/connect/endSession',
+    '/am/oauth2/realms/root/connect/idpEndSession',
     '/am/oauth2/realms/root/realms/middleware/connect/endSession',
     '/am/oauth2/realms/root/realms/tokens-expiring-soon/connect/endSession',
     '/am/oauth2/realms/root/realms/tokens-expired/connect/endSession',
diff --git a/e2e/mock-api/src/app/responses.js b/e2e/mock-api/src/app/responses.js
index 12cc6173d..c38190241 100644
--- a/e2e/mock-api/src/app/responses.js
+++ b/e2e/mock-api/src/app/responses.js
@@ -1032,6 +1032,7 @@ export const wellKnownForgeRock = {
   registration_endpoint: 'http://localhost:9443/am/oauth2/realms/root/register',
 };
 
+// NOT USED
 export const wellKnownPing = {
   issuer: 'https://ping.example.com:9443/02fb4743-189a-4bc7-9d6c-a919edfe6447/as',
   authorization_endpoint:
@@ -1120,6 +1121,87 @@ export const wellKnownPing = {
   code_challenge_methods_supported: ['plain', 'S256'],
 };
 
+export const newPiWellKnown = {
+  issuer: 'http://localhost:9443/am',
+  authorization_endpoint: 'http://localhost:9443/am/oauth2/realms/root/authorize',
+  token_endpoint: 'http://localhost:9443/am/oauth2/realms/root/access_token',
+  userinfo_endpoint: 'http://localhost:9443/am/oauth2/realms/root/userinfo',
+  jwks_uri: 'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/jwks',
+  end_session_endpoint: 'http://localhost:9443/am/oauth2/realms/root/connect/endSession',
+  ping_end_idp_session_endpoint:
+    'http://localhost:9443/am/oauth2/realms/root/connect/idpEndSession',
+  introspection_endpoint: 'http://localhost:9443/am/oauth2/realms/root/introspect',
+  revocation_endpoint: 'http://localhost:9443/am/oauth2/realms/root/token/revoke',
+  claims_parameter_supported: false,
+  request_parameter_supported: true,
+  request_uri_parameter_supported: false,
+  require_pushed_authorization_requests: false,
+  scopes_supported: ['openid', 'profile', 'email', 'address', 'phone', 'offline_access'],
+  response_types_supported: [
+    'code',
+    'id_token',
+    'token id_token',
+    'code id_token',
+    'code token',
+    'code token id_token',
+  ],
+  response_modes_supported: ['pi.flow', 'query', 'fragment', 'form_post'],
+  grant_types_supported: [
+    'authorization_code',
+    'implicit',
+    'client_credentials',
+    'refresh_token',
+    'urn:ietf:params:oauth:grant-type:device_code',
+  ],
+  subject_types_supported: ['public'],
+  id_token_signing_alg_values_supported: ['RS256'],
+  userinfo_signing_alg_values_supported: ['none'],
+  request_object_signing_alg_values_supported: [
+    'none',
+    'HS256',
+    'HS384',
+    'HS512',
+    'RS256',
+    'RS384',
+    'RS512',
+  ],
+  token_endpoint_auth_methods_supported: [
+    'client_secret_basic',
+    'client_secret_post',
+    'client_secret_jwt',
+    'private_key_jwt',
+  ],
+  token_endpoint_auth_signing_alg_values_supported: [
+    'HS256',
+    'HS384',
+    'HS512',
+    'RS256',
+    'RS384',
+    'RS512',
+  ],
+  claim_types_supported: ['normal'],
+  claims_supported: [
+    'sub',
+    'iss',
+    'auth_time',
+    'acr',
+    'name',
+    'given_name',
+    'family_name',
+    'middle_name',
+    'preferred_username',
+    'profile',
+    'picture',
+    'zoneinfo',
+    'phone_number',
+    'updated_at',
+    'address',
+    'email',
+    'locale',
+  ],
+  code_challenge_methods_supported: ['plain', 'S256'],
+};
+
 export const MetadataMarketPlaceInitialize = {
   authId: 'foo',
   callbacks: [
diff --git a/e2e/mock-api/src/app/routes.auth.js b/e2e/mock-api/src/app/routes.auth.js
index 95768433d..f66fbed1f 100644
--- a/e2e/mock-api/src/app/routes.auth.js
+++ b/e2e/mock-api/src/app/routes.auth.js
@@ -44,10 +44,10 @@ import {
   txnAuthz,
   otpQRCodeCallbacks,
   wellKnownForgeRock,
-  wellKnownPing,
   recaptchaEnterpriseCallback,
   MetadataMarketPlaceInitialize,
   MetadataMarketPlacePingOneEvaluation,
+  newPiWellKnown,
 } from './responses.js';
 import initialRegResponse from './response.registration.js';
 import wait from './wait.js';
@@ -520,7 +520,10 @@ export default function (app) {
         redirectUrl.searchParams.set('state', req.query.state);
 
         res.redirect(redirectUrl);
-      } else if (req.cookies.redirected === 'true') {
+      } else if (
+        req.cookies.redirected === 'true' ||
+        req.query['acr_values'] === 'skipBackgroundRequest'
+      ) {
         res.redirect(loginUrl);
       } else {
         res.cookie('redirected', 'true');
@@ -635,7 +638,7 @@ export default function (app) {
     res.send(wellKnownForgeRock);
   });
 
-  app.get('/as/.well-known/oidc-configuration', (req, res) => {
-    res.send(wellKnownPing);
+  app.get('/as/.well-known/new-oidc-configuration', (req, res) => {
+    res.send(newPiWellKnown);
   });
 }
diff --git a/packages/javascript-sdk/src/config/helpers.test.ts b/packages/javascript-sdk/src/config/helpers.test.ts
index c1b2ca168..537c30c55 100644
--- a/packages/javascript-sdk/src/config/helpers.test.ts
+++ b/packages/javascript-sdk/src/config/helpers.test.ts
@@ -1,5 +1,5 @@
 import { convertWellKnown } from './helpers';
-import { frWellKnown, piWellKnown } from './well-known.mock';
+import { frWellKnown, newPiWellKnown, piWellKnown } from './well-known.mock';
 
 describe('Test config helpers', () => {
   it('should test wellknown response conversion ForgeRock', () => {
@@ -33,4 +33,19 @@ describe('Test config helpers', () => {
     };
     expect(result).toStrictEqual(expected);
   });
+
+  it('should test the new wellknown response conversion from Ping', () => {
+    const result = convertWellKnown(newPiWellKnown);
+    const expected = {
+      baseUrl: 'https://auth.pingone.ca',
+      paths: {
+        accessToken: '/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/token',
+        authorize: '/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authorize',
+        endSession: '/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/idpSignoff',
+        revoke: '/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/revoke',
+        userInfo: '/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/userinfo',
+      },
+    };
+    expect(result).toStrictEqual(expected);
+  });
 });
diff --git a/packages/javascript-sdk/src/config/helpers.ts b/packages/javascript-sdk/src/config/helpers.ts
index 5f1c56208..f6f5c0e7c 100644
--- a/packages/javascript-sdk/src/config/helpers.ts
+++ b/packages/javascript-sdk/src/config/helpers.ts
@@ -46,7 +46,7 @@ export function convertWellKnown(data: WellKnownResponse, options?: ServerConfig
     ...(authenticateUrl ? { authenticate: new URL(authenticateUrl).pathname } : {}),
     authorize: new URL(data.authorization_endpoint).pathname,
     accessToken: new URL(data.token_endpoint).pathname,
-    endSession: new URL(data.end_session_endpoint).pathname,
+    endSession: new URL(data.ping_end_idp_session_endpoint || data.end_session_endpoint).pathname,
     userInfo: new URL(data.userinfo_endpoint).pathname,
     revoke: new URL(data.revocation_endpoint).pathname,
     ...(sessionsUrl ? { sessions: new URL(sessionsUrl).pathname } : {}),
diff --git a/packages/javascript-sdk/src/config/interfaces.ts b/packages/javascript-sdk/src/config/interfaces.ts
index 7d183d373..4c0d94a16 100644
--- a/packages/javascript-sdk/src/config/interfaces.ts
+++ b/packages/javascript-sdk/src/config/interfaces.ts
@@ -131,6 +131,7 @@ interface WellKnownResponse {
   token_endpoint: string;
   userinfo_endpoint: string;
   end_session_endpoint: string;
+  ping_end_idp_session_endpoint?: string;
   introspection_endpoint: string;
   revocation_endpoint: string;
   jwks_uri?: string;
diff --git a/packages/javascript-sdk/src/config/well-known.mock.ts b/packages/javascript-sdk/src/config/well-known.mock.ts
index 2a4d955ab..7f1bcdb72 100644
--- a/packages/javascript-sdk/src/config/well-known.mock.ts
+++ b/packages/javascript-sdk/src/config/well-known.mock.ts
@@ -418,3 +418,90 @@ export const piWellKnown = {
   ],
   code_challenge_methods_supported: ['plain', 'S256'],
 };
+
+export const newPiWellKnown = {
+  issuer: 'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as',
+  authorization_endpoint:
+    'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authorize',
+  pushed_authorization_request_endpoint:
+    'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/par',
+  token_endpoint: 'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/token',
+  userinfo_endpoint: 'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/userinfo',
+  jwks_uri: 'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/jwks',
+  end_session_endpoint: 'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/signoff',
+  ping_end_idp_session_endpoint:
+    'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/idpSignoff',
+  introspection_endpoint:
+    'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/introspect',
+  revocation_endpoint: 'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/revoke',
+  device_authorization_endpoint:
+    'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/device_authorization',
+  claims_parameter_supported: false,
+  request_parameter_supported: true,
+  request_uri_parameter_supported: false,
+  require_pushed_authorization_requests: false,
+  scopes_supported: ['openid', 'profile', 'email', 'address', 'phone', 'offline_access'],
+  response_types_supported: [
+    'code',
+    'id_token',
+    'token id_token',
+    'code id_token',
+    'code token',
+    'code token id_token',
+  ],
+  response_modes_supported: ['pi.flow', 'query', 'fragment', 'form_post'],
+  grant_types_supported: [
+    'authorization_code',
+    'implicit',
+    'client_credentials',
+    'refresh_token',
+    'urn:ietf:params:oauth:grant-type:device_code',
+  ],
+  subject_types_supported: ['public'],
+  id_token_signing_alg_values_supported: ['RS256'],
+  userinfo_signing_alg_values_supported: ['none'],
+  request_object_signing_alg_values_supported: [
+    'none',
+    'HS256',
+    'HS384',
+    'HS512',
+    'RS256',
+    'RS384',
+    'RS512',
+  ],
+  token_endpoint_auth_methods_supported: [
+    'client_secret_basic',
+    'client_secret_post',
+    'client_secret_jwt',
+    'private_key_jwt',
+  ],
+  token_endpoint_auth_signing_alg_values_supported: [
+    'HS256',
+    'HS384',
+    'HS512',
+    'RS256',
+    'RS384',
+    'RS512',
+  ],
+  claim_types_supported: ['normal'],
+  claims_supported: [
+    'sub',
+    'iss',
+    'auth_time',
+    'acr',
+    'name',
+    'given_name',
+    'family_name',
+    'middle_name',
+    'preferred_username',
+    'profile',
+    'picture',
+    'zoneinfo',
+    'phone_number',
+    'updated_at',
+    'address',
+    'email',
+    'locale',
+  ],
+  code_challenge_methods_supported: ['plain', 'S256'],
+};
diff --git a/packages/javascript-sdk/src/fr-user/index.ts b/packages/javascript-sdk/src/fr-user/index.ts
index c69054aa6..9bc10ef8e 100644
--- a/packages/javascript-sdk/src/fr-user/index.ts
+++ b/packages/javascript-sdk/src/fr-user/index.ts
@@ -16,7 +16,9 @@ import OAuth2Client from '../oauth2-client';
 import SessionManager from '../session-manager';
 import TokenManager from '../token-manager';
 import type { LogoutOptions } from '../oauth2-client/interfaces';
+import Config from '../config';
 import TokenStorage from '../token-storage';
+import { getEndpointPath } from '../util/url';
 
 /**
  * High-level API for logging a user in/out and getting profile information.
@@ -47,13 +49,25 @@ abstract class FRUser {
     // Shallow copy options to delete redirect prop
     const configOptions = { ...options };
     delete configOptions.redirect;
+    const { realmPath, serverConfig } = Config.get(configOptions);
 
-    // Just log any exceptions that are thrown, but don't abandon the flow
-    try {
-      // Both invalidates the session on the server AND removes browser cookie
-      await SessionManager.logout(configOptions);
-    } catch (error) {
-      FRLogger.warn('Session logout was not successful');
+    if (getEndpointPath('sessions', realmPath, serverConfig.paths)) {
+      // Just log any exceptions that are thrown, but don't abandon the flow
+      try {
+        // Both invalidates the session on the server AND removes browser cookie
+        await SessionManager.logout(configOptions);
+      } catch (error) {
+        FRLogger.warn('Session logout was not successful');
+      }
+    }
+
+    if (options?.redirect === false) {
+      try {
+        // Invalidates session on the server tied to the ID Token
+        await OAuth2Client.endSession({ ...options });
+      } catch (error) {
+        FRLogger.warn('OAuth endSession was not successful');
+      }
     }
 
     try {
@@ -69,14 +83,13 @@ abstract class FRUser {
     // Remove tokens locally
     await TokenManager.deleteTokens();
 
-    // Do this last as it can result in a redirect if using PingOne
-    try {
-      // Invalidates session on the server tied to the ID Token
-      // Needed for Express environment as session logout is unavailable
-      // Pass in the original `options` as it's needed for redirect control
-      await OAuth2Client.endSession({ ...options, idToken });
-    } catch (error) {
-      FRLogger.warn('OAuth endSession was not successful');
+    if (options?.redirect !== false) {
+      try {
+        // Pass in the original `options` as it's needed for redirect control
+        await OAuth2Client.endSession({ ...options, idToken });
+      } catch (error) {
+        FRLogger.warn('OAuth endSession was not successful');
+      }
     }
   }
 }
diff --git a/packages/javascript-sdk/src/oauth2-client/index.ts b/packages/javascript-sdk/src/oauth2-client/index.ts
index e6c705e40..ee98cb8de 100644
--- a/packages/javascript-sdk/src/oauth2-client/index.ts
+++ b/packages/javascript-sdk/src/oauth2-client/index.ts
@@ -309,12 +309,13 @@ abstract class OAuth2Client {
     };
 
     init = init || ({} as RequestInit);
+    init.headers = (init.headers || new Headers()) as Headers;
+    init.headers.set('Accept', 'application/json');
 
     if (includeToken) {
       const tokens = await TokenStorage.get();
       const accessToken = tokens && tokens.accessToken;
       init.credentials = 'include';
-      init.headers = (init.headers || new Headers()) as Headers;
       init.headers.set('Authorization', `Bearer ${accessToken}`);
     }
     const runMiddleware = middlewareWrapper(
diff --git a/packages/javascript-sdk/src/token-manager/index.ts b/packages/javascript-sdk/src/token-manager/index.ts
index 3d553b78f..00f7d83b5 100644
--- a/packages/javascript-sdk/src/token-manager/index.ts
+++ b/packages/javascript-sdk/src/token-manager/index.ts
@@ -22,15 +22,28 @@ import { tokensWillExpireWithinThreshold } from './helpers';
 interface GetTokensOptions extends ConfigOptions {
   forceRenew?: boolean;
   login?: 'embedded' | 'redirect' | undefined;
+  skipBackgroundRequest?: boolean;
   query?: StringDict<string>;
 }
 
+/**
+ * Token Manager class that provides high-level abstraction for Authorization Code flow,
+ * PKCE value generation, token exchange and token storage.
+ *
+ * Supports both embedded authentication as well as external authentication via redirects
+ */
 abstract class TokenManager {
   /**
-   * Token Manager class that provides high-level abstraction for Authorization Code flow,
-   * PKCE value generation, token exchange and token storage.
-   *
-   * Supports both embedded authentication as well as external authentication via redirects
+   * @function getTokens - Retrieves OAuth2 tokens from the server or local storage.
+   * @param {Object} options - Options for retrieving tokens.
+   * @param {boolean} [options.forceRenew] - If true, forces a new token request even if tokens are already stored.
+   * @param {string} [options.login] - Specifies the type of login: 'embedded' or 'redirect'.
+   * @param {boolean} [options.skipBackgroundRequest] - If true, skips the background request to get tokens without redirect.
+   * @param {Object} [options.query] - Query key-value pairs to convert to URL parameters for the /authorize request.
+   * @param {string} [options.query.code] - Authorization code from the redirect URL.
+   * @param {string} [options.query.state] - State parameter from the redirect URL.
+   * @returns {Promise<OAuth2Tokens | void>} - Returns a promise that resolves to the retrieved tokens or void.
+   * @throws {Error} - Throws an error if the client ID is not provided, if tokens cannot be exchanged, or if there is a state mismatch.
    *
    Example 1:
 
@@ -63,6 +76,14 @@ abstract class TokenManager {
      },
    });
    ```
+
+   Example 4:
+
+   ```js
+   const tokens = forgerock.TokenManager.getTokens({
+     skipBackgroundRequest: true, // OPTIONAL; this will skip the iframe request to silently get tokens w/o redirect
+   });
+   ```
    */
   public static async getTokens(options?: GetTokensOptions): Promise<OAuth2Tokens | void> {
     const { clientId, oauthThreshold, prefix } = Config.get(options as ConfigOptions);
@@ -132,68 +153,79 @@ abstract class TokenManager {
       responseType: ResponseType.Code,
     });
 
-    /**
-     * Attempt to call the authorize URL to retrieve authorization code
-     */
-    try {
-      // Check expected browser support
-      // To support legacy browsers, iframe works best with short timeout
-      const parsedUrl = new URL(await OAuth2Client.getAuthCodeByIframe(pkceValues));
-
-      // Throw if we have an error param or have no authorization code
-      if (parsedUrl.searchParams.get('error')) {
-        throw Error(`${parsedUrl.searchParams.get('error_description')}`);
-      } else if (!parsedUrl.searchParams.get('code')) {
-        throw Error(allowedErrors.AuthenticationConsentRequired);
-      }
+    if (!options) {
+      options = {};
+    }
 
-      const parsedQuery = parseQuery(parsedUrl.toString());
+    if (options.skipBackgroundRequest !== true) {
+      /**
+       * Attempt to call the authorize URL to retrieve authorization code
+       */
+      try {
+        // Check expected browser support
+        // To support legacy browsers, iframe works best with short timeout
+        const parsedUrl = new URL(await OAuth2Client.getAuthCodeByIframe(pkceValues));
 
-      if (!options) {
-        options = {};
-      }
-      options.query = parsedQuery;
-    } catch (err) {
-      // If authorize request fails, handle according to `login` type
-      if (!(err instanceof Error) || options?.login !== 'redirect') {
-        // Throw for any error if login is NOT of type "redirect"
-        throw err;
-      }
+        // Throw if we have an error param or have no authorization code
+        if (parsedUrl.searchParams.get('error')) {
+          throw Error(`${parsedUrl.searchParams.get('error_description')}`);
+        } else if (!parsedUrl.searchParams.get('code')) {
+          throw Error(allowedErrors.AuthenticationConsentRequired);
+        }
 
-      // Check if error is not one of our allowed errors
-      if (
-        allowedErrors.AuthenticationIsRequired !== err.message &&
-        allowedErrors.AuthenticationConsentRequired !== err.message &&
-        allowedErrors.AuthorizationTimeout !== err.message &&
-        allowedErrors.FailedToFetch !== err.message &&
-        allowedErrors.NetworkError !== err.message &&
-        allowedErrors.InteractionNotAllowed !== err.message &&
-        allowedErrors.RequestRequiresConsent !== err.message &&
-        // Check for Ping Identity Login Required error
-        // Long message, so just check substring
-        !err.message.includes(allowedErrors.LoginRequired) &&
-        // Safari has a very long error message, so we check for a substring
-        !err.message.includes(allowedErrors.CORSError)
-      ) {
-        // Throw if the error is NOT an explicitly allowed error along with redirect of true
-        // as that is a normal response and just requires a redirect
-        throw err;
-      }
+        const parsedQuery = parseQuery(parsedUrl.toString());
+
+        options.query = parsedQuery;
+      } catch (err) {
+        // If authorize request fails, handle according to `login` type
+        if (!(err instanceof Error) || options?.login !== 'redirect') {
+          // Throw for any error if login is NOT of type "redirect"
+          throw err;
+        }
+
+        // Check if error is not one of our allowed errors
+        if (
+          allowedErrors.AuthenticationIsRequired !== err.message &&
+          allowedErrors.AuthenticationConsentRequired !== err.message &&
+          allowedErrors.AuthorizationTimeout !== err.message &&
+          allowedErrors.FailedToFetch !== err.message &&
+          allowedErrors.NetworkError !== err.message &&
+          allowedErrors.InteractionNotAllowed !== err.message &&
+          allowedErrors.RequestRequiresConsent !== err.message &&
+          // Check for Ping Identity Login Required error
+          // Long message, so just check substring
+          !err.message.includes(allowedErrors.LoginRequired) &&
+          // Safari has a very long error message, so we check for a substring
+          !err.message.includes(allowedErrors.CORSError)
+        ) {
+          // Throw if the error is NOT an explicitly allowed error along with redirect of true
+          // as that is a normal response and just requires a redirect
+          throw err;
+        }
+
+        const authorizeUrl = await OAuth2Client.createAuthorizeUrl(pkceValues);
 
-      const authorizeUrl = await OAuth2Client.createAuthorizeUrl(pkceValues);
+        // Before redirecting, store PKCE values
+        storePkceValues();
 
-      // Before redirecting, store PKCE values
-      storePkceValues();
+        return location.assign(authorizeUrl);
+      }
 
-      return location.assign(authorizeUrl);
+      /**
+       * Exchange authorization code for tokens
+       */
+      return await this.tokenExchange(options, {
+        state: pkceValues.state,
+        verifier: pkceValues.verifier,
+      });
     }
-    /**
-     * Exchange authorization code for tokens
-     */
-    return await this.tokenExchange(options, {
-      state: pkceValues.state,
-      verifier: pkceValues.verifier,
-    });
+
+    const authorizeUrl = await OAuth2Client.createAuthorizeUrl(pkceValues);
+
+    // Before redirecting, store PKCE values
+    storePkceValues();
+
+    return location.assign(authorizeUrl);
   }
 
   public static async deleteTokens(): Promise<void> {


Reviews:
user: login: ryanbas21
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:04:49Z
user: login: ryanbas21
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:06:27Z
user: login: cerebrl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T20:00:57Z
user: login: cerebrl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T22:08:23Z
user: login: ryanbas21
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T22:24:07Z
user: login: ryanbas21
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T21:05:16Z

Review comments:
diff_hunk: @@ -0,0 +1,145 @@
+/*
+ * @forgerock/javascript-sdk
+ *
+ * autoscript.ts
+ *
+ * Copyright (c) 2020 ForgeRock. All rights reserved.
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+// @ts-nocheck
+import * as forgerock from '@forgerock/javascript-sdk';
+import { delay as rxDelay, map, mergeMap } from 'rxjs/operators';
+import { from } from 'rxjs';
+
+function autoscript() {
+  const delay = 0;
+
+  const url = new URL(window.location.href);
+  const amUrl = url.searchParams.get('amUrl') || 'http://localhost:9443/am';
+  const preAuthenticated = url.searchParams.get('preAuthenticated') || 'false';
+  const code = url.searchParams.get('code') || '';
+  const clientId = url.searchParams.get('clientId');
+  const client_id = url.searchParams.get('client_id');
+  const error = url.searchParams.get('error_description') || false;
+  const realmPath = url.searchParams.get('realmPath') || 'root';
+  const scope = url.searchParams.get('scope') || 'openid profile me.read';
+  const state = url.searchParams.get('state') || '';
+  const acr_values = url.searchParams.get('acr') || 'skipBackgroundRequest';
+  // in central login we use an auth query param for the return of our mock 401 request
+  // this is to prevent the evaluation of the page before we have technically authenticated
+  const auth = url.searchParams.get('auth') || false;
+
+  let tokenStore = url.searchParams.get('tokenStore') || 'localStorage';
+
+  // Support full redirects by setting storage, rather than rely purely on URL
+  if (!localStorage.getItem('tokenStore')) {
+    localStorage.setItem('tokenStore', tokenStore);
+  } else {
+    tokenStore = localStorage.getItem('tokenStore');
+  }
+
+  console.log('Configure the SDK');
+  forgerock.Config.set({
+    clientId: clientId || client_id || 'CentralLoginOAuthClient',
+    realmPath,
+    redirectUri: `${url.origin}/src/${
+      preAuthenticated === 'false' ? 'authn-central-login' : '_callback'
+    }/`,
+    scope,
+    serverConfig: {
+      baseUrl: amUrl,
+    },
+    tokenStore,
+  });
+
+  if (!code && !state) {
+    try {
+      forgerock.SessionManager.logout();
+    } catch (err) {
+      // Do nothing
+    }
+  }
+
+  console.log('Initiate first step with `undefined`');
+
+  // Wrapping in setTimeout to give the test time to bind listener to console.log
+  setTimeout(() => {
+    from([1])
+      .pipe(
+        map(() => {
+          if (preAuthenticated === 'true') {
+            console.log('Set mock cookie to represent existing session');
+            document.cookie = 'iPlanetDirectoryPro=abcd1234; domain=localhost; path=/';
+            if (code && state) {
+              window.sessionStorage.setItem(
+                `FR-SDK-${clientId}`,
+                JSON.stringify({ responseType: 'code', state, verifier: '1234' }),
+              );
+            }
+          }
+          return;
+        }),
+        rxDelay(delay),
+        mergeMap((step) => {
+          let tokens;
+          // detect when in iframe, throw as error if so
+          if (window.self !== window.top) {
+            throw new Error('Loaded_In_Iframe');
+          } else if (code && state) {
+            tokens = forgerock.TokenManager.getTokens({
+              query: { code, state, acr_values },
+            });
+          } else {
+            tokens = forgerock.TokenManager.getTokens({
+              skipBackgroundRequest: true,
path: e2e/autoscript-apps/src/authn-central-login-no-iframe/autoscript.ts
user: login: ryanbas21
body: i'm concerned with this naming, i just fear another `modern` | `legacy` situation where people will read this name and say yeah we want to skip this.


created_at: 2025-04-15T12:04:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,6 +22,7 @@ import { tokensWillExpireWithinThreshold } from './helpers';
 interface GetTokensOptions extends ConfigOptions {
   forceRenew?: boolean;
   login?: 'embedded' | 'redirect' | undefined;
+  skipBackgroundRequest?: boolean;
path: packages/javascript-sdk/src/token-manager/index.ts
user: login: ryanbas21
body: I think we should JS Doc comment this `GetTokensOptions` interface so that the IDE gets more intellisense on this type. Especially this new addition.

created_at: 2025-04-15T12:06:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,6 +22,7 @@ import { tokensWillExpireWithinThreshold } from './helpers';
 interface GetTokensOptions extends ConfigOptions {
   forceRenew?: boolean;
   login?: 'embedded' | 'redirect' | undefined;
+  skipBackgroundRequest?: boolean;
path: packages/javascript-sdk/src/token-manager/index.ts
user: login: cerebrl
body: Done
created_at: 2025-04-15T20:00:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,145 @@
+/*
+ * @forgerock/javascript-sdk
+ *
+ * autoscript.ts
+ *
+ * Copyright (c) 2020 ForgeRock. All rights reserved.
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+// @ts-nocheck
+import * as forgerock from '@forgerock/javascript-sdk';
+import { delay as rxDelay, map, mergeMap } from 'rxjs/operators';
+import { from } from 'rxjs';
+
+function autoscript() {
+  const delay = 0;
+
+  const url = new URL(window.location.href);
+  const amUrl = url.searchParams.get('amUrl') || 'http://localhost:9443/am';
+  const preAuthenticated = url.searchParams.get('preAuthenticated') || 'false';
+  const code = url.searchParams.get('code') || '';
+  const clientId = url.searchParams.get('clientId');
+  const client_id = url.searchParams.get('client_id');
+  const error = url.searchParams.get('error_description') || false;
+  const realmPath = url.searchParams.get('realmPath') || 'root';
+  const scope = url.searchParams.get('scope') || 'openid profile me.read';
+  const state = url.searchParams.get('state') || '';
+  const acr_values = url.searchParams.get('acr') || 'skipBackgroundRequest';
+  // in central login we use an auth query param for the return of our mock 401 request
+  // this is to prevent the evaluation of the page before we have technically authenticated
+  const auth = url.searchParams.get('auth') || false;
+
+  let tokenStore = url.searchParams.get('tokenStore') || 'localStorage';
+
+  // Support full redirects by setting storage, rather than rely purely on URL
+  if (!localStorage.getItem('tokenStore')) {
+    localStorage.setItem('tokenStore', tokenStore);
+  } else {
+    tokenStore = localStorage.getItem('tokenStore');
+  }
+
+  console.log('Configure the SDK');
+  forgerock.Config.set({
+    clientId: clientId || client_id || 'CentralLoginOAuthClient',
+    realmPath,
+    redirectUri: `${url.origin}/src/${
+      preAuthenticated === 'false' ? 'authn-central-login' : '_callback'
+    }/`,
+    scope,
+    serverConfig: {
+      baseUrl: amUrl,
+    },
+    tokenStore,
+  });
+
+  if (!code && !state) {
+    try {
+      forgerock.SessionManager.logout();
+    } catch (err) {
+      // Do nothing
+    }
+  }
+
+  console.log('Initiate first step with `undefined`');
+
+  // Wrapping in setTimeout to give the test time to bind listener to console.log
+  setTimeout(() => {
+    from([1])
+      .pipe(
+        map(() => {
+          if (preAuthenticated === 'true') {
+            console.log('Set mock cookie to represent existing session');
+            document.cookie = 'iPlanetDirectoryPro=abcd1234; domain=localhost; path=/';
+            if (code && state) {
+              window.sessionStorage.setItem(
+                `FR-SDK-${clientId}`,
+                JSON.stringify({ responseType: 'code', state, verifier: '1234' }),
+              );
+            }
+          }
+          return;
+        }),
+        rxDelay(delay),
+        mergeMap((step) => {
+          let tokens;
+          // detect when in iframe, throw as error if so
+          if (window.self !== window.top) {
+            throw new Error('Loaded_In_Iframe');
+          } else if (code && state) {
+            tokens = forgerock.TokenManager.getTokens({
+              query: { code, state, acr_values },
+            });
+          } else {
+            tokens = forgerock.TokenManager.getTokens({
+              skipBackgroundRequest: true,
path: e2e/autoscript-apps/src/authn-central-login-no-iframe/autoscript.ts
user: login: cerebrl
body: As mentioned on Slack. I think this is okay, and unintentially using this option won't do any harm. I've added more information in the JSDoc to cover this option's intention.
created_at: 2025-04-15T22:08:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-10T02:46:06Z
body: ###  🦋  Changeset detected

Latest commit: bac217706a8630cd7519309c8517db6a398fd4ba

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                      | Type  |
| ------------------------- | ----- |
| @forgerock/javascript-sdk | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/ForgeRock/forgerock-javascript-sdk/new/feat_add-new-signout-endpoint?filename=.changeset/clever-carrots-enjoy.md&value=---%0A%22%40forgerock%2Fjavascript-sdk%22%3A%20patch%0A%22autoscript-apps%22%3A%20patch%0A%22autoscript-suites%22%3A%20patch%0A%22mock-api%22%3A%20patch%0A---%0A%0Afeat(javascript-sdk)%3A%20ping%20one%20signout%2C%20flag%20for%20iframe%20skip%2C%20remove%20session%20call%0A)


author_association: NONE
user: login: nx-cloud[bot]
created_at: 2025-04-10T02:46:54Z
body: 
View your [CI Pipeline Execution ↗](https://cloud.nx.app/cipes/68001618d0a42172b052e8a6?utm_source=pull-request&utm_medium=comment) for commit bac217706a8630cd7519309c8517db6a398fd4ba.

| Command | Status | Duration | Result |
|---------|--------|----------:|--------|
| `nx affected -t build lint test docs e2e-ci --ve...` | ✅ Succeeded | 4m 54s | [View ↗](https://cloud.nx.app/runs/iTCGbgIf7K?utm_source=pull-request&utm_medium=comment) |
| `nx-cloud record -- nx format:check --verbose` | ✅ Succeeded | 6s | [View ↗](https://cloud.nx.app/runs/49DkN9N10h?utm_source=pull-request&utm_medium=comment) |

---

☁️ [Nx Cloud](https://cloud.nx.app?utm_source=pull-request&utm_medium=comment) last updated this comment at `2025-04-16 20:47:23` UTC

<!-- NX_CLOUD_APP_COMMENT_END -->
author_association: NONE
user: login: codecov-commenter
created_at: 2025-04-15T11:44:30Z
body: ## [Codecov](https://app.codecov.io/gh/ForgeRock/forgerock-javascript-sdk/pull/535?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ForgeRock) Report
Attention: Patch coverage is `67.30769%` with `68 lines` in your changes missing coverage. Please review.
> Project coverage is 58.47%. Comparing base [(`f890af3`)](https://app.codecov.io/gh/ForgeRock/forgerock-javascript-sdk/commit/f890af3ac2738f9c3583797d3cba8133b83b31dc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ForgeRock) to head [(`bac2177`)](https://app.codecov.io/gh/ForgeRock/forgerock-javascript-sdk/commit/bac217706a8630cd7519309c8517db6a398fd4ba?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ForgeRock).

| [Files with missing lines](https://app.codecov.io/gh/ForgeRock/forgerock-javascript-sdk/pull/535?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ForgeRock) | Patch % | Lines |
|---|---|---|
| [packages/javascript-sdk/src/token-manager/index.ts](https://app.codecov.io/gh/ForgeRock/forgerock-javascript-sdk/pull/535?src=pr&el=tree&filepath=packages%2Fjavascript-sdk%2Fsrc%2Ftoken-manager%2Findex.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ForgeRock#diff-cGFja2FnZXMvamF2YXNjcmlwdC1zZGsvc3JjL3Rva2VuLW1hbmFnZXIvaW5kZXgudHM=) | 54.94% | [41 Missing :warning: ](https://app.codecov.io/gh/ForgeRock/forgerock-javascript-sdk/pull/535?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ForgeRock) |
| [packages/javascript-sdk/src/fr-user/index.ts](https://app.codecov.io/gh/ForgeRock/forgerock-javascript-sdk/pull/535?src=pr&el=tree&filepath=packages%2Fjavascript-sdk%2Fsrc%2Ffr-user%2Findex.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ForgeRock#diff-cGFja2FnZXMvamF2YXNjcmlwdC1zZGsvc3JjL2ZyLXVzZXIvaW5kZXgudHM=) | 7.40% | [25 Missing :warning: ](https://app.codecov.io/gh/ForgeRock/forgerock-javascript-sdk/pull/535?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ForgeRock) |
| [packages/javascript-sdk/src/oauth2-client/index.ts](https://app.codecov.io/gh/ForgeRock/forgerock-javascript-sdk/pull/535?src=pr&el=tree&filepath=packages%2Fjavascript-sdk%2Fsrc%2Foauth2-client%2Findex.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ForgeRock#diff-cGFja2FnZXMvamF2YXNjcmlwdC1zZGsvc3JjL29hdXRoMi1jbGllbnQvaW5kZXgudHM=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/ForgeRock/forgerock-javascript-sdk/pull/535?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ForgeRock) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop     #535      +/-   ##
===========================================
+ Coverage    58.33%   58.47%   +0.14%     
===========================================
  Files          105      105              
  Lines        25174    25307     +133     
  Branches      1688     1690       +2     
===========================================
+ Hits         14685    14799     +114     
- Misses       10489    10508      +19     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ForgeRock/forgerock-javascript-sdk/pull/535?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ForgeRock).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ForgeRock).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: ryanbas21
created_at: 2025-04-15T22:24:16Z
body: Reviewed / Tested.
author_association: CONTRIBUTOR