diff --git a/README.md b/README.md
index 4e6f08b1..a3deb270 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,9 @@ so typically a hand-written user interface will be chosen over a generic machine
These panes are used in the Data Browser - see mashlib
[https://github.com/linkeddata/mashlib](https://github.com/linkeddata/mashlib)
+When panes are hosted through mashlib and use solid-logic authentication, ensure the refresh worker is served same-origin. The worker export and runtime override contract are documented in solid-logic:
+https://github.com/solidos/solid-logic#worker-asset-and-runtime-configuration
+
Currently the panes available include:
- A default pane which lists the properties of any object
diff --git a/dev/loader.ts b/dev/loader.ts
index 6636a2f5..fb843e80 100644
--- a/dev/loader.ts
+++ b/dev/loader.ts
@@ -130,22 +130,22 @@ window.onload = async () => {
// registerPanes((cjsOrEsModule: any) => paneRegistry.register(cjsOrEsModule.default || cjsOrEsModule))
const contactsPane = await import('contacts-pane')
paneRegistry.register((contactsPane as any).default || contactsPane)
- await authSession.handleIncomingRedirect({
- restorePreviousSession: true
- })
- const session = await authSession
- if (!session.info.isLoggedIn) {
+ await solidLogicSingleton.authn.checkUser()
+ const session = authSession
+ const isLoggedIn = session?.info?.isLoggedIn ?? session?.isActive ?? Boolean(session?.webId)
+ if (!isLoggedIn) {
console.log('The user is not logged in')
const loginBanner = document.getElementById('loginBanner');
if (loginBanner) {
loginBanner.innerHTML = '';
}
} else {
- console.log(`Logged in as ${session.info.webId}`)
+ const loggedWebId = session?.info?.webId || session?.webId
+ console.log(`Logged in as ${loggedWebId}`)
const loginBanner = document.getElementById('loginBanner');
if (loginBanner) {
- loginBanner.innerHTML = `Logged in as ${session.info.webId} `;
+ loginBanner.innerHTML = `Logged in as ${loggedWebId} `;
}
}
addLayoutButtons()
@@ -156,8 +156,9 @@ window.logout = () => {
window.location.href = ''
}
window.login = async function () {
- const session = await authSession
- if (!session.info.isLoggedIn) {
+ const session = authSession
+ const isLoggedIn = session?.info?.isLoggedIn ?? session?.isActive ?? Boolean(session?.webId)
+ if (!isLoggedIn) {
const issuer = prompt('Please enter an issuer URI', 'https://solidcommunity.net')
if (issuer) {
await authSession.login({
diff --git a/jest.config.mjs b/jest.config.mjs
index 7d1c1632..5e114e41 100644
--- a/jest.config.mjs
+++ b/jest.config.mjs
@@ -1,3 +1,12 @@
+import { existsSync } from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+const localSolidLogicIndex = path.resolve(__dirname, '../solid-logic/src/index.ts')
+const useLocalSolidLogic = existsSync(localSolidLogicIndex)
+
export default {
collectCoverage: true,
coverageDirectory: 'coverage',
@@ -19,7 +28,11 @@ export default {
'\\.svg\\?raw$': '/test/__mocks__/fileMock.js',
'\\.(svg)$': '/test/__mocks__/fileMock.js',
'\\.(png|jpe?g|gif|webp|avif)$': '/test/__mocks__/fileMock.js',
- '\\.css$': '/test/__mocks__/styleMock.js'
+ '\\.css$': '/test/__mocks__/styleMock.js',
+ ...(useLocalSolidLogic ? { '^solid-logic$': localSolidLogicIndex } : {}),
+ '^@uvdsl/solid-oidc-client-browser$': '/test/mocks/solid-oidc-client-browser.ts',
+ '^@uvdsl/solid-oidc-client-browser/core$': '/test/mocks/solid-oidc-client-browser.ts',
+ '^solid-oidc-client-browser$': '/test/mocks/solid-oidc-client-browser.ts'
},
setupFilesAfterEnv: ['./test/helpers/setup.ts'],
testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
diff --git a/src/mainPage/header.ts b/src/mainPage/header.ts
index f6546e7f..b525c268 100644
--- a/src/mainPage/header.ts
+++ b/src/mainPage/header.ts
@@ -38,6 +38,33 @@ export const HELP_MENU_LIST = [
]
const HEADER_MOBILE_STYLE_ID = 'solid-ui-header-mobile-style'
+let authRefreshInFlight: Promise | null = null
+
+async function ensureAuthUserResolved (): Promise {
+ if (authn.currentUser()) return
+ if (authRefreshInFlight) {
+ await authRefreshInFlight
+ return
+ }
+
+ authRefreshInFlight = (async () => {
+ try {
+ await authn.checkUser()
+ // Some auth stacks resolve session state asynchronously after first check.
+ if (!authn.currentUser()) {
+ await authn.checkUser()
+ }
+ } catch (_err) {
+ // Keep header rendering resilient when auth refresh fails.
+ }
+ })()
+
+ try {
+ await authRefreshInFlight
+ } finally {
+ authRefreshInFlight = null
+ }
+}
type ManagedHeader = Header & {
__solidPanesListenersAttached?: boolean
@@ -139,6 +166,7 @@ function attachHeaderListeners (header: ManagedHeader) {
export async function refreshHeader (outliner: OutlineManager, headerElement?: Header) {
ensureMobileHeaderStyles()
+ await ensureAuthUserResolved()
const headerOptions = setHeaderOptions(outliner)
const header = headerElement || document.querySelector('solid-ui-header') as Header | null
if (!header) return null
diff --git a/src/mainPage/menu.ts b/src/mainPage/menu.ts
index b43be7a4..a6261f11 100644
--- a/src/mainPage/menu.ts
+++ b/src/mainPage/menu.ts
@@ -59,7 +59,11 @@ const applyMenuCollapsedState = (navMenu: HTMLElement | null): void => {
updateCollapseButtonPosition(navMenu, collapseBtn)
}
-const isLoggedIn = (): boolean => Boolean(authSession?.info?.isLoggedIn)
+// Compatibility: solid-logic Session shape differs across stacks (info.isLoggedIn vs isActive/webId).
+const isLoggedIn = (): boolean => {
+ const sessionAny = authSession as any
+ return Boolean(sessionAny?.info?.isLoggedIn ?? sessionAny?.isActive ?? sessionAny?.webId)
+}
const setFooterVisibility = (loggedIn: boolean): void => {
const footer = document.querySelector('solid-ui-footer') as HTMLElement | null
diff --git a/test/mocks/solid-oidc-client-browser.ts b/test/mocks/solid-oidc-client-browser.ts
new file mode 100644
index 00000000..7e883922
--- /dev/null
+++ b/test/mocks/solid-oidc-client-browser.ts
@@ -0,0 +1,59 @@
+type Listener = (...args: any[]) => void
+
+class EventEmitterLike {
+ private listeners: Record = {}
+
+ on (event: string, listener: Listener): void {
+ const list = this.listeners[event] || []
+ list.push(listener)
+ this.listeners[event] = list
+ }
+
+ emit (event: string, ...args: any[]): void {
+ const list = this.listeners[event] || []
+ list.forEach(listener => listener(...args))
+ }
+}
+
+export class Session {
+ info: { webId?: string, isLoggedIn: boolean } = { isLoggedIn: false }
+ webId?: string
+ isActive = false
+ events = new EventEmitterLike()
+
+ addEventListener (event: string, listener: Listener): void {
+ this.events.on(event, listener)
+ }
+
+ async handleIncomingRedirect (): Promise {
+
+ }
+
+ async handleRedirectFromLogin (): Promise {
+
+ }
+
+ async restore (): Promise {
+
+ }
+
+ async login (): Promise {
+
+ }
+
+ async logout (): Promise {
+ this.info = { isLoggedIn: false }
+ this.webId = undefined
+ this.isActive = false
+ }
+
+ fetch (input: RequestInfo | URL, init?: RequestInit): Promise {
+ return globalThis.fetch(input, init)
+ }
+
+ authFetch (input: RequestInfo | URL, init?: RequestInit): Promise {
+ return globalThis.fetch(input, init)
+ }
+}
+
+export class SessionCore extends Session {}