diff --git a/examples/tanstack-start/src/routeTree.gen.ts b/examples/tanstack-start/src/routeTree.gen.ts index 0a8aa6b1..6b63a1df 100644 --- a/examples/tanstack-start/src/routeTree.gen.ts +++ b/examples/tanstack-start/src/routeTree.gen.ts @@ -88,7 +88,7 @@ export interface FileRoutesByFullPath { '/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute '/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute '/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute - '/demo/start/ssr': typeof DemoStartSsrIndexRoute + '/demo/start/ssr/': typeof DemoStartSsrIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -130,7 +130,7 @@ export interface FileRouteTypes { | '/demo/start/ssr/data-only' | '/demo/start/ssr/full-ssr' | '/demo/start/ssr/spa-mode' - | '/demo/start/ssr' + | '/demo/start/ssr/' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -227,7 +227,7 @@ declare module '@tanstack/react-router' { '/demo/start/ssr/': { id: '/demo/start/ssr/' path: '/demo/start/ssr' - fullPath: '/demo/start/ssr' + fullPath: '/demo/start/ssr/' preLoaderRoute: typeof DemoStartSsrIndexRouteImport parentRoute: typeof rootRouteImport } diff --git a/package.json b/package.json index fec71007..f89fd8ee 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oidc-spa", - "version": "10.2.0", + "version": "10.3.0-rc.1", "description": "OpenID Connect / OAuth2 solution for client-first Web Applications", "repository": { "type": "git", diff --git a/scripts/start-example.sh b/scripts/start-example.sh index 0297d5bf..5c25023b 100755 --- a/scripts/start-example.sh +++ b/scripts/start-example.sh @@ -12,6 +12,7 @@ yarn rm -rf node_modules/oidc-spa cp -r ../../dist node_modules/oidc-spa rm -rf node_modules/.vite +rm -rf node_modules/.vite-temp rm -rf .angular/cache yarn $2 diff --git a/src/core/createOidc.ts b/src/core/createOidc.ts index 88e2ace8..11ed8aa8 100644 --- a/src/core/createOidc.ts +++ b/src/core/createOidc.ts @@ -7,6 +7,7 @@ import { import { type OidcMetadata, fetchOidcMetadata } from "./OidcMetadata"; import { assert, type Equals } from "../tools/tsafe/assert"; import { id } from "../tools/tsafe/id"; +import { Reflect } from "../tools/tsafe/Reflect"; import { Deferred } from "../tools/Deferred"; import { createEvtIsUserActive } from "./evtIsUserActive"; import { createStartCountdown } from "../tools/startCountdown"; @@ -266,14 +267,51 @@ export type ParamsOfCreateOidc< disableDPoP?: true; }; -const globalContext = { - prOidcByConfigId: new Map>>(), - hasLogoutBeenCalled: id(false), - dExports_earlyInit: new Deferred(), - dExports_tokenSubstitution: new Deferred(), - dExports_DPoP: new Deferred() +type GlobalContextActual = { + prOidcByConfigId: Map>>; + hasLogoutBeenCalled: boolean; + exports_earlyInit: Exports_earlyInit; + exports_tokenSubstitution: Exports_tokenSubstitution | undefined; + exports_DPoP: Exports_DPoP | undefined; }; +declare global { + interface Window { + "__oidc-spa:globalContext:createOidc": { + dEarlyInitResolved: Deferred; + actual_exposedForMicroFrontendSetup: + | GlobalContextActual + | "not a micro frontend setup" + | undefined; + }; + } +} + +window["__oidc-spa:globalContext:createOidc"] ??= { + dEarlyInitResolved: new Deferred(), + actual_exposedForMicroFrontendSetup: undefined +}; + +const globalContext = window["__oidc-spa:globalContext:createOidc"]; + +const globalContext_actual_moduleScoped: GlobalContextActual = { + prOidcByConfigId: new Map(), + hasLogoutBeenCalled: false, + exports_earlyInit: Reflect(), + exports_tokenSubstitution: undefined, + exports_DPoP: undefined +}; + +function getGlobalContextActual(): GlobalContextActual { + assert(globalContext.actual_exposedForMicroFrontendSetup !== undefined); + + if (globalContext.actual_exposedForMicroFrontendSetup === "not a micro frontend setup") { + return globalContext_actual_moduleScoped; + } + + return globalContext.actual_exposedForMicroFrontendSetup; +} + export type Exports_earlyInit = | { shouldLoadApp: false } | { @@ -287,8 +325,23 @@ export type Exports_earlyInit = sessionRestorationMethod: "iframe" | "full page redirect" | "auto" | undefined; }; -export function registerExports_earlyInit(exports: Exports_earlyInit): void { - globalContext.dExports_earlyInit.resolve(exports); +export function registerExports_earlyInit(params: { + exports: Exports_earlyInit; + isMicroFrontendSetup: boolean; +}): void { + const { exports, isMicroFrontendSetup } = params; + + if (globalContext.actual_exposedForMicroFrontendSetup !== undefined) { + throw new Error("oidc-spa: Wrong micro frontend setup"); + } + + globalContext.actual_exposedForMicroFrontendSetup = isMicroFrontendSetup + ? globalContext_actual_moduleScoped + : "not a micro frontend setup"; + + globalContext_actual_moduleScoped.exports_earlyInit = exports; + + globalContext.dEarlyInitResolved.resolve(); } export type Exports_tokenSubstitution = { @@ -307,7 +360,8 @@ export namespace Exports_tokenSubstitution { } export function registerExports_tokenSubstitution(exports: Exports_tokenSubstitution): void { - globalContext.dExports_tokenSubstitution.resolve(exports); + assert(!globalContext_actual_moduleScoped.exports_earlyInit); + globalContext_actual_moduleScoped.exports_tokenSubstitution = exports; } export type Exports_DPoP = { @@ -339,7 +393,8 @@ export namespace Exports_DPoP { } export function registerExports_DPoP(exports: Exports_DPoP): void { - globalContext.dExports_DPoP.resolve(exports); + assert(!globalContext_actual_moduleScoped.exports_earlyInit); + globalContext_actual_moduleScoped.exports_DPoP = exports; } /** @see: https://docs.oidc-spa.dev/v/v10/usage */ @@ -384,7 +439,30 @@ export async function createOidc< const configId = getConfigId({ issuerUri, clientId }); - const { prOidcByConfigId } = globalContext; + wait_early_init: { + const { dEarlyInitResolved } = globalContext; + + if (dEarlyInitResolved.getState().hasResolved) { + break wait_early_init; + } + + const timer = window.setTimeout(() => { + console.warn( + [ + "oidc-spa: Setup error.", + "oidcEarlyInit() wasn't called.", + "This is supposed to be handled by the oidc-spa Vite plugin", + "or manually in other environments." + ].join(" ") + ); + }, 3_000); + + await dEarlyInitResolved.pr; + + window.clearTimeout(timer); + } + + const { prOidcByConfigId } = getGlobalContextActual(); use_previous_instance: { const prOidc = prOidcByConfigId.get(configId); @@ -452,24 +530,7 @@ export async function createOidc_nonMemoized< BASE_URL: BASE_URL_params } = params; - const exports_earlyInit = await (async () => { - const timer = window.setTimeout(() => { - console.warn( - [ - "oidc-spa: Setup error.", - "oidcEarlyInit() wasn't called.", - "This is supposed to be handled by the oidc-spa Vite plugin", - "or manually in other environments." - ].join(" ") - ); - }, 3_000); - - const exports_earlyInit = await globalContext.dExports_earlyInit.pr; - - window.clearTimeout(timer); - - return exports_earlyInit; - })(); + const { exports_earlyInit } = getGlobalContextActual(); if (!exports_earlyInit.shouldLoadApp) { return new Promise(() => {}); @@ -484,9 +545,9 @@ export async function createOidc_nonMemoized< const sessionRestorationMethod = sessionRestorationMethod_params ?? sessionRestorationMethod_earlyInit ?? "auto"; - const { value: exports_tokenSubstitution } = globalContext.dExports_tokenSubstitution.getState(); + const { exports_tokenSubstitution } = getGlobalContextActual(); - const { value: exports_DPoP } = globalContext.dExports_DPoP.getState(); + const { exports_DPoP } = getGlobalContextActual(); const scopes = Array.from(new Set(["openid", ...(params.scopes ?? ["profile"])])); @@ -1532,12 +1593,12 @@ export async function createOidc_nonMemoized< }, getDecodedIdToken: () => currentTokens.decodedIdToken, logout: async params => { - if (globalContext.hasLogoutBeenCalled) { + if (getGlobalContextActual().hasLogoutBeenCalled) { log?.("logout() has already been called, ignoring the call"); return new Promise(() => {}); } - globalContext.hasLogoutBeenCalled = true; + getGlobalContextActual().hasLogoutBeenCalled = true; const rootRelativePostLogoutRedirectUrl: string = (() => { switch (params.redirectTo) { diff --git a/src/core/earlyInit.ts b/src/core/earlyInit.ts index 39bea195..e3fb902d 100644 --- a/src/core/earlyInit.ts +++ b/src/core/earlyInit.ts @@ -58,6 +58,9 @@ export type ParamsOfEarlyInit = { enableDPoP?: () => void; enableTokenSubstitution?: () => void; }; + + /** Default: false */ + isMicroFrontendSetup?: boolean; }; let shouldLoadApp: boolean | undefined = undefined; @@ -73,7 +76,12 @@ export function oidcEarlyInit(params?: ParamsOfEarlyInit) { } function oidcEarlyInit_nonMemoized(params: ParamsOfEarlyInit | undefined) { - const { BASE_URL, sessionRestorationMethod, securityDefenses = {} } = params ?? {}; + const { + BASE_URL, + sessionRestorationMethod, + securityDefenses = {}, + isMicroFrontendSetup = false + } = params ?? {}; if (!isBrowser) { return { shouldLoadApp: true }; @@ -185,7 +193,7 @@ function oidcEarlyInit_nonMemoized(params: ParamsOfEarlyInit | undefined) { } prModuleCreateOidc.then(({ registerExports_earlyInit }) => { - registerExports_earlyInit(exports_earlyInit); + registerExports_earlyInit({ exports: exports_earlyInit, isMicroFrontendSetup }); }); return { shouldLoadApp }; diff --git a/src/core/evtIsUserActive.ts b/src/core/evtIsUserActive.ts index cadfe522..d0bbee83 100644 --- a/src/core/evtIsUserActive.ts +++ b/src/core/evtIsUserActive.ts @@ -3,11 +3,22 @@ import { subscribeToUserInteraction } from "../tools/subscribeToUserInteraction" import { assert, is } from "../tools/tsafe/assert"; import { id } from "../tools/tsafe/id"; -const globalContext = { +declare global { + interface Window { + "__oidc-spa:globalContext:evtIsUserActive": { + appInstanceId: string; + evtIsUserActiveBySessionId: Map>; + }; + } +} + +window["__oidc-spa:globalContext:evtIsUserActive"] ??= { appInstanceId: Math.random().toString(36).slice(2), evtIsUserActiveBySessionId: new Map>() }; +const globalContext = window["__oidc-spa:globalContext:evtIsUserActive"]; + type EventData = { isUserActive: true } | { isUserActive: false; hasBeenInactiveForHowLongMs: number }; export function createEvtIsUserActive(params: { diff --git a/src/core/loginOrGoToAuthServer.ts b/src/core/loginOrGoToAuthServer.ts index 74eb3157..f07c1922 100644 --- a/src/core/loginOrGoToAuthServer.ts +++ b/src/core/loginOrGoToAuthServer.ts @@ -4,16 +4,26 @@ import { assert, type Equals } from "../tools/tsafe/assert"; import { noUndefined } from "../tools/tsafe/noUndefined"; import type { StateData } from "./StateData"; import type { NonPostableEvt } from "../tools/Evt"; -import { createStatefulEvt } from "../tools/StatefulEvt"; +import { createStatefulEvt, StatefulEvt } from "../tools/StatefulEvt"; import { Deferred } from "../tools/Deferred"; import { addOrUpdateSearchParam, getAllSearchParams } from "../tools/urlSearchParams"; import { getIsOnline } from "../tools/getIsOnline"; import { setStateDataCookieIfEnabled } from "./StateDataCookie"; -const globalContext = { +declare global { + interface Window { + "__oidc-spa:globalContext:loginOrGoToAuthServer": { + evtHasLoginBeenCalled: StatefulEvt; + }; + } +} + +window["__oidc-spa:globalContext:loginOrGoToAuthServer"] ??= { evtHasLoginBeenCalled: createStatefulEvt(() => false) }; +const globalContext = window["__oidc-spa:globalContext:loginOrGoToAuthServer"]; + type Params = Params.Login | Params.GoToAuthServer; namespace Params { diff --git a/src/core/loginPropagationToOtherTabs.ts b/src/core/loginPropagationToOtherTabs.ts index 926919ed..fe749c9c 100644 --- a/src/core/loginPropagationToOtherTabs.ts +++ b/src/core/loginPropagationToOtherTabs.ts @@ -1,10 +1,20 @@ import { assert, is } from "../tools/tsafe/assert"; import { Deferred } from "../tools/Deferred"; -const globalContext = { +declare global { + interface Window { + "__oidc-spa.createOidc.loginPropagationToOtherTabs": { + appInstanceId: string; + }; + } +} + +window["__oidc-spa.createOidc.loginPropagationToOtherTabs"] ??= { appInstanceId: Math.random().toString(36).slice(2) }; +const globalContext = window["__oidc-spa.createOidc.loginPropagationToOtherTabs"]; + type Message = { appInstanceId: string; configId: string; diff --git a/src/core/logoutPropagationToOtherTabs.ts b/src/core/logoutPropagationToOtherTabs.ts index 0bec9f1f..b151925b 100644 --- a/src/core/logoutPropagationToOtherTabs.ts +++ b/src/core/logoutPropagationToOtherTabs.ts @@ -1,10 +1,20 @@ import { assert, is } from "../tools/tsafe/assert"; import { Deferred } from "../tools/Deferred"; -const globalContext = { +declare global { + interface Window { + "__oidc-spa:globalContext:logoutPropagationToOtherTabs": { + appInstanceId: string; + }; + } +} + +window["__oidc-spa:globalContext:logoutPropagationToOtherTabs"] ??= { appInstanceId: Math.random().toString(36).slice(2) }; +const globalContext = window["__oidc-spa:globalContext:logoutPropagationToOtherTabs"]; + type Message = { appInstanceId: string; configId: string; diff --git a/src/core/ongoingLoginOrRefreshProcesses.ts b/src/core/ongoingLoginOrRefreshProcesses.ts index 9b99f2cf..402ec357 100644 --- a/src/core/ongoingLoginOrRefreshProcesses.ts +++ b/src/core/ongoingLoginOrRefreshProcesses.ts @@ -1,12 +1,22 @@ import { Deferred } from "../tools/Deferred"; import { assert } from "../tools/tsafe/assert"; -import { id } from "../tools/tsafe/id"; -const globalContext = { - prDone_arr: id[]>([]), - prUnlock: id>(Promise.resolve()) +declare global { + interface Window { + "__oidc-spa:globalContext:ongoingLoginOrRefreshProcesses": { + prDone_arr: Promise[]; + prUnlock: Promise; + }; + } +} + +window["__oidc-spa:globalContext:ongoingLoginOrRefreshProcesses"] ??= { + prDone_arr: [], + prUnlock: Promise.resolve() }; +const globalContext = window["__oidc-spa:globalContext:ongoingLoginOrRefreshProcesses"]; + export async function startLoginOrRefreshProcess(): Promise<{ completeLoginOrRefreshProcess: () => void; }> {