Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 48 additions & 8 deletions src/typeIndex/typeIndexLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,22 @@ export function createTypeIndexLogic(store, authn, profileLogic, utilityLogic):
if (!user) throw new Error('loadTypeIndexesFor: No user given')
const profile = await profileLogic.loadProfile(user)

const suggestion = suggestPublicTypeIndex(user)
let suggestion: NamedNode | null = null
try {
suggestion = suggestPublicTypeIndex(user)
} catch (err) {
const message = `User ${user} has no usable profile document directory for publicTypeIndex.`
debug.warn(message)
}
let publicTypeIndex
try {
publicTypeIndex = await utilityLogic.followOrCreateLink(user, ns.solid('publicTypeIndex') as NamedNode, suggestion, profile)
publicTypeIndex =
store.any(user, ns.solid('publicTypeIndex'), undefined, profile) ||
(suggestion
? await utilityLogic.followOrCreateLink(user, ns.solid('publicTypeIndex') as NamedNode, suggestion, profile)
: null)
} catch (err) {
const message = `User ${user} has no pointer in profile to publicTypeIndex file.`
const message = `User ${user} has no pointer in profile to publicTypeIndex file: ${err}`
debug.warn(message)
}
const publicScopes = publicTypeIndex ? [{ label: 'public', index: publicTypeIndex as NamedNode, agent: user }] : []
Expand All @@ -40,13 +50,21 @@ export function createTypeIndexLogic(store, authn, profileLogic, utilityLogic):
if (preferencesFile) { // watch out - can be in either as spec was not clear. Legacy is profile.
// If there is a legacy one linked from the profile, use that.
// Otherwiae use or make one linked from Preferences
const suggestedPrivateTypeIndex = suggestPrivateTypeIndex(preferencesFile)
let suggestedPrivateTypeIndex: NamedNode | null = null
try {
suggestedPrivateTypeIndex = suggestPrivateTypeIndex(preferencesFile)
} catch (err) {
const message = `User ${user} has no usable preferences document directory for privateTypeIndex.`
debug.warn(message)
}
let privateTypeIndex
try {
privateTypeIndex = store.any(user, ns.solid('privateTypeIndex'), undefined, profile) ||
await utilityLogic.followOrCreateLink(user, ns.solid('privateTypeIndex') as NamedNode, suggestedPrivateTypeIndex, preferencesFile)
(suggestedPrivateTypeIndex
? await utilityLogic.followOrCreateLink(user, ns.solid('privateTypeIndex') as NamedNode, suggestedPrivateTypeIndex, preferencesFile)
: null)
} catch (err) {
const message = `User ${user} has no pointer in preference file to privateTypeIndex file.`
const message = `User ${user} has no pointer in preference file to privateTypeIndex file: ${err}`
debug.warn(message)
}
privateScopes = privateTypeIndex ? [{ label: 'private', index: privateTypeIndex as NamedNode, agent: user }] : []
Expand Down Expand Up @@ -109,13 +127,35 @@ export function createTypeIndexLogic(store, authn, profileLogic, utilityLogic):
return scopedAppInstances.map(scoped => scoped.instance)
}

function docDirUri(node: NamedNode): string | null {
const doc = node.doc()
const dir = doc.dir()
if (dir?.uri) return dir.uri
const docUri = doc.uri
if (!docUri) {
debug.log(`docDirUri: missing doc uri for ${node?.uri}`)
return null
}
const withoutFragment = docUri.split('#')[0]
const lastSlash = withoutFragment.lastIndexOf('/')
if (lastSlash === -1) {
debug.log(`docDirUri: no slash in doc uri ${docUri}`)
return null
}
return withoutFragment.slice(0, lastSlash + 1)
Comment on lines +140 to +145
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docDirUri’s string-splitting fallback can return an invalid directory for WebIDs whose doc URI has no path beyond the origin (e.g. https://example.com#me => https://). In that scenario suggest*TypeIndex will build an invalid URL and may even write bad links. Use URL parsing instead (e.g. derive the directory via new URL('.', docUriWithoutFragment).toString() and ensure origin-only URIs map to origin + '/').

Suggested change
const lastSlash = withoutFragment.lastIndexOf('/')
if (lastSlash === -1) {
debug.log(`docDirUri: no slash in doc uri ${docUri}`)
return null
}
return withoutFragment.slice(0, lastSlash + 1)
try {
const docUrl = new URL(withoutFragment)
const dirUrl = new URL('.', docUrl)
const dirUri = dirUrl.toString()
// Ensure origin-only URIs map to "origin + '/'" (e.g. https://example.com -> https://example.com/)
if (dirUrl.origin && (dirUrl.pathname === '' || dirUrl.pathname === '/')) {
return dirUrl.origin + '/'
}
return dirUri
} catch (e) {
debug.log(`docDirUri: invalid doc uri ${docUri}`, e)
return null
}

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this case never appears. So no need to change code

}

function suggestPublicTypeIndex(me: NamedNode) {
return sym(me.doc().dir()?.uri + 'publicTypeIndex.ttl')
const dirUri = docDirUri(me)
if (!dirUri) throw new Error(`suggestPublicTypeIndex: Cannot derive directory for ${me.uri}`)
return sym(dirUri + 'publicTypeIndex.ttl')
}
// Note this one is based off the pref file not the profile

function suggestPrivateTypeIndex(preferencesFile: NamedNode) {
return sym(preferencesFile.doc().dir()?.uri + 'privateTypeIndex.ttl')
const dirUri = docDirUri(preferencesFile)
if (!dirUri) throw new Error(`suggestPrivateTypeIndex: Cannot derive directory for ${preferencesFile.uri}`)
return sym(dirUri + 'privateTypeIndex.ttl')
}

/*
Expand Down
48 changes: 46 additions & 2 deletions test/typeIndexLogic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @jest-environment jsdom
*
*/
import { Fetcher, Store, sym, UpdateManager } from 'rdflib'
import { Fetcher, parse, Store, sym, UpdateManager } from 'rdflib'
import { createAclLogic } from '../src/acl/aclLogic'
import { createProfileLogic } from '../src/profile/profileLogic'
import { createTypeIndexLogic} from '../src/typeIndex/typeIndexLogic'
Expand Down Expand Up @@ -51,7 +51,7 @@ describe('TypeIndex logic NEW', () => {
requests = []
statustoBeReturned = 200

fetchMock.mockIf(/^https?.*$/, async req => {
fetchMock.mockIf(/^(https?|mailto):.*$/, async req => {

if (req.method !== 'GET') {
requests.push(req)
Expand Down Expand Up @@ -131,6 +131,50 @@ describe('TypeIndex logic NEW', () => {
expect(store.statementsMatching(null, null, null, AlicePrivateTypeIndex).length).toEqual(8)
expect(store.statementsMatching(null, null, null, AlicePublicTypeIndex).length).toEqual(8)
})
it('uses existing publicTypeIndex when suggestion fails', async () => {
const carol = sym('urn:uuid:carol#me')
const CarolProfileDoc = carol.doc()
const CarolPreferencesFile = sym('https://carol.example.com/settings/prefs.ttl')
const CarolPublicTypeIndex = sym('https://carol.example.com/profile/public-type-index.ttl')
const CarolPrivateTypeIndex = sym('https://carol.example.com/settings/private-type-index.ttl')

const CarolProfile = `
<#me> a vcard:Individual;
space:preferencesFile ${CarolPreferencesFile};
solid:publicTypeIndex ${CarolPublicTypeIndex}.
`
const CarolPreferences = `
${carol} solid:privateTypeIndex ${CarolPrivateTypeIndex} .
`

web[CarolPreferencesFile.uri] = CarolPreferences
web[CarolPublicTypeIndex.uri] = `
:t solid:forClass wf:Tracker; solid:instance <../publicStuff/actionItems.ttl#this> .
`
web[CarolPrivateTypeIndex.uri] = `
:t solid:forClass wf:Tracker; solid:instance <../privateStuff/ToDo.ttl#this> .
`

const util = createUtilityLogic(store, createAclLogic(store), createContainerLogic(store))
const profileLogic = {
loadProfile: async (user) => {
parse(prefixes + CarolProfile, store, CarolProfileDoc.uri, 'text/turtle')
return user.doc()
},
silencedLoadPreferences: async () => {
parse(prefixes + CarolPreferences, store, CarolPreferencesFile.uri, 'text/turtle')
return CarolPreferencesFile
}
}
const typeIndexLogicWithStub = createTypeIndexLogic(store, authn, profileLogic as any, util)

const result = await typeIndexLogicWithStub.loadTypeIndexesFor(carol)
expect(result).toEqual([
{ label: 'public', index: CarolPublicTypeIndex as any, agent: carol as any },
{ label: 'private', index: CarolPrivateTypeIndex as any, agent: carol as any }
])
expect(requests.length).toEqual(0)
})
})

const ClubScopes =
Expand Down