From 2633f95062fbc7a8e61e74b62ba855087af2ecdb Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Mon, 16 Mar 2026 07:09:40 +1100 Subject: [PATCH 1/5] improved missing pref messaging --- src/login/login.ts | 62 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/src/login/login.ts b/src/login/login.ts index b93e07f1..87716328 100644 --- a/src/login/login.ts +++ b/src/login/login.ts @@ -65,6 +65,16 @@ const { deleteTypeIndexRegistration } = solidLogicSingleton.typeIndex +function formatDynamicError (error: unknown, contextLabel: string): string { + const e = error as any + const name = e?.name || 'Error' + const status = typeof e?.status === 'number' ? ` (HTTP ${e.status})` : '' + const message = e?.message + ? String(e.message) + : (typeof error === 'string' ? error : 'No additional details provided') + return `${contextLabel}: ${name}${status} - ${message}` +} + /** * Resolves with the logged in user's WebID * @@ -125,6 +135,10 @@ export async function ensureLoadedPreferences ( } */ try { context = await ensureLoadedProfile(context) + if (!context.me) { + context.preferencesFileError = 'Not logged in, so preferences were not loaded.' + return context + } // console.log('back in Solid UI after logInLoadProfile', context) const preferencesFile = await loadPreferences(context.me as NamedNode) @@ -137,30 +151,47 @@ export async function ensureLoadedPreferences ( if (err instanceof UnauthorizedError) { m2 = 'Oops — you are not authenticated (properly logged in), so SolidOS cannot read your preferences file. Try logging out and then logging back in.' + context.preferencesFileError = m2 alert(m2) + return context } else if (err instanceof CrossOriginForbiddenError) { - m2 = `Unauthorized: Assuming preference file blocked for origin ${window.location.origin}` + m2 = `Unauthorized: preference file request was blocked for origin ${window.location.origin}.` context.preferencesFileError = m2 return context } else if (err instanceof SameOriginForbiddenError) { m2 = - 'You are not authorized to read your preference file. This may be because you are using an untrusted web app.' + 'You are not authorized to read your preference file from this app context.' + context.preferencesFileError = m2 debug.warn(m2) return context } else if (err instanceof NotEditableError) { m2 = - 'You are not authorized to edit your preference file. This may be because you are using an untrusted web app.' + 'You are not authorized to edit your preference file from this app context.' + context.preferencesFileError = m2 debug.warn(m2) return context } else if (err instanceof WebOperationError) { - m2 = - 'You are not authorized to edit your preference file. This may be because you are using an untrusted web app.' + m2 = formatDynamicError(err, 'Preference file web operation failed') + context.preferencesFileError = m2 debug.warn(m2) + return context } else if (err instanceof FetchError) { - m2 = `Strange: Error ${err.status} trying to read your preference file.${err.message}` + if (err.status === 404) { + m2 = 'Your preferences file was not found (404). It may not exist yet.' + context.preferencesFileError = m2 + debug.warn(m2) + return context + } + m2 = formatDynamicError(err, 'Error reading your preferences file') + context.preferencesFileError = m2 + debug.warn(m2) alert(m2) + return context } else { - throw new Error(`(via loadPrefs) ${err}`) + m2 = formatDynamicError(err, 'Unexpected error while loading preferences') + context.preferencesFileError = m2 + debug.error(m2) + return context } } return context @@ -183,14 +214,17 @@ export async function ensureLoadedProfile ( try { const logInContext = await ensureLoggedIn(context) if (!logInContext.me) { - throw new Error('Could not log in') + const message = 'Could not log in; skipping profile load.' + debug.log(message) + return context } context.publicProfile = await loadProfile(logInContext.me) } catch (err) { + const message = formatDynamicError(err, 'Unable to load your profile') if (context.div && context.dom) { - context.div.appendChild(widgets.errorMessageBlock(context.dom, err.message)) + context.div.appendChild(widgets.errorMessageBlock(context.dom, message)) } - throw new Error(`Can't log in: ${err}`) + throw new Error(message) } return context } @@ -1049,9 +1083,13 @@ export function newAppInstance ( export async function getUserRoles (): Promise> { try { const { me, preferencesFile, preferencesFileError } = await ensureLoadedPreferences({}) - if (!preferencesFile || preferencesFileError) { + if (preferencesFileError) { throw new Error(preferencesFileError) } + if (!preferencesFile) { + const authState = me ? `logged in as ${me.value || me.uri}` : 'not logged in' + throw new Error(`Preferences file unavailable (${authState})`) + } return solidLogicSingleton.store.each( me, ns.rdf('type'), @@ -1059,7 +1097,7 @@ export async function getUserRoles (): Promise> { preferencesFile.doc() ) as NamedNode[] } catch (error) { - debug.warn('Unable to fetch your preferences - this was the error: ', error) + debug.warn(formatDynamicError(error, 'Unable to fetch your preferences')) } return [] } From 640df55a85a0abd13c4b6072aa1c025c28f25025 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Mon, 16 Mar 2026 07:15:52 +1100 Subject: [PATCH 2/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/login/login.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/login/login.ts b/src/login/login.ts index 87716328..757e656e 100644 --- a/src/login/login.ts +++ b/src/login/login.ts @@ -216,7 +216,7 @@ export async function ensureLoadedProfile ( if (!logInContext.me) { const message = 'Could not log in; skipping profile load.' debug.log(message) - return context + throw new Error(message) } context.publicProfile = await loadProfile(logInContext.me) } catch (err) { From 1713b898045ccb2463ea029512ddd6a730423455 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Mon, 16 Mar 2026 07:16:35 +1100 Subject: [PATCH 3/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/login/login.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/login/login.ts b/src/login/login.ts index 757e656e..c7909996 100644 --- a/src/login/login.ts +++ b/src/login/login.ts @@ -155,7 +155,7 @@ export async function ensureLoadedPreferences ( alert(m2) return context } else if (err instanceof CrossOriginForbiddenError) { - m2 = `Unauthorized: preference file request was blocked for origin ${window.location.origin}.` + m2 = `Blocked by origin: preference file request from ${window.location.origin} was forbidden. Ensure this app origin is trusted/allowed for your preferences file.` context.preferencesFileError = m2 return context } else if (err instanceof SameOriginForbiddenError) { From e73dba15c94ede927a035105b2d3143afaa0682d Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Mon, 16 Mar 2026 07:29:19 +1100 Subject: [PATCH 4/5] offer to create pref --- src/login/login.ts | 81 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/login/login.ts b/src/login/login.ts index c7909996..a26f33cc 100644 --- a/src/login/login.ts +++ b/src/login/login.ts @@ -75,6 +75,82 @@ function formatDynamicError (error: unknown, contextLabel: string): string { return `${contextLabel}: ${name}${status} - ${message}` } +function guessPreferencesFileUri (me: NamedNode): string { + return me.uri + .replace('/profile/', '/') + .replace('/public/', '/') + .split('/') + .slice(0, -1) + .join('/') + '/Settings/Preferences.ttl' +} + +function getMissingPreferencesFileUri (context: AuthenticationContext, err: unknown): string | null { + const responseUrl = (err as any)?.response?.url + if (typeof responseUrl === 'string' && responseUrl) { + return responseUrl + } + if (context.me) { + const pointer = store.any( + context.me, + ns.space('preferencesFile'), + undefined, + context.me.doc ? context.me.doc() : undefined + ) as NamedNode | null + if (pointer?.uri) { + return pointer.uri + } + return guessPreferencesFileUri(context.me as NamedNode) + } + return null +} + +async function createPreferencesFile (context: AuthenticationContext, uri: string): Promise { + if (!store.fetcher) { + throw new Error('Cannot create preferences file: store has no fetcher') + } + const initialData = '# Preferences file created by solid-ui\n' + await store.fetcher.webOperation('PUT', uri, { + data: initialData, + contentType: 'text/turtle' + }) + if (!context.me) { + throw new Error('Cannot reload preferences file: no logged-in WebID') + } + context.preferencesFile = await loadPreferences(context.me as NamedNode) + context.preferencesFileError = undefined +} + +function offerCreatePreferencesFile ( + context: AuthenticationContext, + uri: string, + baseMessage: string +): void { + if (context.div && context.dom) { + const box = context.dom.createElement('div') + box.appendChild(widgets.errorMessageBlock(context.dom, `${baseMessage} Create it now?`)) + const createButton = context.dom.createElement('button') + createButton.textContent = 'Create preferences file' + createButton.setAttribute('style', style.buttonStyle) + createButton.addEventListener('click', () => { + createButton.disabled = true + createPreferencesFile(context, uri) + .then(() => { + box.appendChild(widgets.errorMessageBlock(context.dom as HTMLDocument, `Preferences file created: ${uri}`, '#dfd')) + }) + .catch(error => { + const message = formatDynamicError(error, 'Failed to create preferences file') + box.appendChild(widgets.errorMessageBlock(context.dom as HTMLDocument, message)) + debug.error(message) + createButton.disabled = false + }) + }) + box.appendChild(createButton) + context.div.appendChild(box) + return + } + debug.warn(`${baseMessage} You can create it at: ${uri}`) +} + /** * Resolves with the logged in user's WebID * @@ -137,6 +213,7 @@ export async function ensureLoadedPreferences ( context = await ensureLoadedProfile(context) if (!context.me) { context.preferencesFileError = 'Not logged in, so preferences were not loaded.' + debug.log('not logged in, no preferences loaded') return context } @@ -180,6 +257,10 @@ export async function ensureLoadedPreferences ( m2 = 'Your preferences file was not found (404). It may not exist yet.' context.preferencesFileError = m2 debug.warn(m2) + const missingPreferencesFileUri = getMissingPreferencesFileUri(context, err) + if (missingPreferencesFileUri) { + offerCreatePreferencesFile(context, missingPreferencesFileUri, m2) + } return context } m2 = formatDynamicError(err, 'Error reading your preferences file') From 3e5699d4a4f0b923d9f382e4ad95ee3e5cc3931c Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Tue, 17 Mar 2026 14:29:52 +1100 Subject: [PATCH 5/5] create prefs do not prompt user --- src/login/login.ts | 123 +++++++++++++++++++++++++++++++-------------- 1 file changed, 84 insertions(+), 39 deletions(-) diff --git a/src/login/login.ts b/src/login/login.ts index a26f33cc..1ba342b1 100644 --- a/src/login/login.ts +++ b/src/login/login.ts @@ -81,7 +81,7 @@ function guessPreferencesFileUri (me: NamedNode): string { .replace('/public/', '/') .split('/') .slice(0, -1) - .join('/') + '/Settings/Preferences.ttl' + .join('/') + '/settings/prefs.ttl' } function getMissingPreferencesFileUri (context: AuthenticationContext, err: unknown): string | null { @@ -104,51 +104,84 @@ function getMissingPreferencesFileUri (context: AuthenticationContext, err: unkn return null } -async function createPreferencesFile (context: AuthenticationContext, uri: string): Promise { +async function ensurePreferencesFileRowInProfile ( + me: NamedNode, + prefsNode: NamedNode, + profileDoc: NamedNode +): Promise { + const existingPreferencesFile = store.any(me, ns.space('preferencesFile'), undefined, profileDoc) + + if (existingPreferencesFile && existingPreferencesFile.equals(prefsNode)) { + return + } + + if (!store.updater) { + throw new Error('Cannot create preferences file: store has no updater') + } + + const deletions = existingPreferencesFile + ? [st(me, ns.space('preferencesFile'), existingPreferencesFile, profileDoc)] + : [] + const insertions = [st(me, ns.space('preferencesFile'), prefsNode, profileDoc)] + + await new Promise((resolve, reject) => { + store.updater!.update(deletions as any, insertions as any, (_uri, ok, body) => { + if (ok) { + resolve() + } else { + reject(new Error(body || 'Unable to update preferencesFile in WebID profile')) + } + }) + }) +} + +function buildPreferencesFileTurtle (me: NamedNode): string { + const mbox = store.any(me, ns.foaf('mbox')) + const mboxLine = mbox ? `\n<${me.uri}> foaf:mbox <${mbox.value}> .\n` : '\n' + + return ( + '@prefix dct: .\n' + + '@prefix pim: .\n' + + '@prefix foaf: .\n' + + '@prefix solid: .\n\n' + + '<>\n' + + ' a pim:ConfigurationFile;\n\n' + + ' dct:title "Preferences file" .\n' + + mboxLine + + `<${me.uri}>\n` + + ' solid:publicTypeIndex ;\n' + + ' solid:privateTypeIndex .\n' + ) +} + +async function writePreferencesFileDocument (uri: string, data: string): Promise { if (!store.fetcher) { throw new Error('Cannot create preferences file: store has no fetcher') } - const initialData = '# Preferences file created by solid-ui\n' + await store.fetcher.webOperation('PUT', uri, { - data: initialData, + data, contentType: 'text/turtle' }) - if (!context.me) { - throw new Error('Cannot reload preferences file: no logged-in WebID') - } - context.preferencesFile = await loadPreferences(context.me as NamedNode) - context.preferencesFileError = undefined } -function offerCreatePreferencesFile ( - context: AuthenticationContext, - uri: string, - baseMessage: string -): void { - if (context.div && context.dom) { - const box = context.dom.createElement('div') - box.appendChild(widgets.errorMessageBlock(context.dom, `${baseMessage} Create it now?`)) - const createButton = context.dom.createElement('button') - createButton.textContent = 'Create preferences file' - createButton.setAttribute('style', style.buttonStyle) - createButton.addEventListener('click', () => { - createButton.disabled = true - createPreferencesFile(context, uri) - .then(() => { - box.appendChild(widgets.errorMessageBlock(context.dom as HTMLDocument, `Preferences file created: ${uri}`, '#dfd')) - }) - .catch(error => { - const message = formatDynamicError(error, 'Failed to create preferences file') - box.appendChild(widgets.errorMessageBlock(context.dom as HTMLDocument, message)) - debug.error(message) - createButton.disabled = false - }) - }) - box.appendChild(createButton) - context.div.appendChild(box) - return +async function createPreferencesFile (context: AuthenticationContext, uri: string): Promise { + const me = (context.me as NamedNode) || authn.currentUser() + if (!me) { + throw new Error('Cannot create preferences file: no logged-in WebID') } - debug.warn(`${baseMessage} You can create it at: ${uri}`) + context.me = me + + const profileDoc = me.doc() + const prefsNode = store.sym(uri) + + await ensurePreferencesFileRowInProfile(me, prefsNode, profileDoc) + + const prefsTurtle = buildPreferencesFileTurtle(me) + await writePreferencesFileDocument(uri, prefsTurtle) + + context.preferencesFile = await loadPreferences(me) + context.preferencesFileError = undefined } /** @@ -255,12 +288,24 @@ export async function ensureLoadedPreferences ( } else if (err instanceof FetchError) { if (err.status === 404) { m2 = 'Your preferences file was not found (404). It may not exist yet.' - context.preferencesFileError = m2 debug.warn(m2) const missingPreferencesFileUri = getMissingPreferencesFileUri(context, err) if (missingPreferencesFileUri) { - offerCreatePreferencesFile(context, missingPreferencesFileUri, m2) + try { + await createPreferencesFile(context, missingPreferencesFileUri) + debug.log(`Preferences file created automatically: ${missingPreferencesFileUri}`) + return context + } catch (createError) { + const message = formatDynamicError(createError, 'Failed to create preferences file automatically') + context.preferencesFileError = message + debug.error(message) + if (context.div && context.dom) { + context.div.appendChild(widgets.errorMessageBlock(context.dom, message)) + } + return context + } } + context.preferencesFileError = m2 return context } m2 = formatDynamicError(err, 'Error reading your preferences file')