Use .jsonld for all generated pod files (#282)#283
Conversation
Pod creation now writes: profile/card.jsonld (was: profile/card, HTML wrapper) settings/prefs.jsonld (was: settings/Preferences.ttl) settings/publicTypeIndex.jsonld (was: settings/publicTypeIndex.ttl) settings/privateTypeIndex.jsonld (was: settings/privateTypeIndex.ttl) The WebID URL changes accordingly: before: https://host/alice/profile/card#me after: https://host/alice/profile/card.jsonld#me Rationale (full discussion in JavaScriptSolidServer#282): JSS is a JSON-LD-native server (Turtle is opt-in via --conneg, JSON-LD is always-on), and clients that want to write back to these files have to assume one extension. Mixed .ttl + .jsonld in the wild produces split chains — pilot and JSS-created pods didn't round-trip cleanly. .jsonld for everything fixes that. Files touched: - src/webid/profile.js: generateProfile() now returns the JSON-LD object directly; HTML shell removed (operators who want a personal-site shell can serve their own index.html with an embedded data island). - src/handlers/container.js (POST /.pods): write .jsonld names, build WebID with .jsonld extension. - src/server.js: same for createRootPodStructure (single-user root pod); ActivityPub actor route + auth bypass paths updated to /profile/card.jsonld. - src/idp/interactions.js: WebID built with .jsonld extension. - src/db/index.js: ownership check uses the new WebID shape. - test/{webid,pod,auth}.test.js: updated to assert the new paths and parse the profile as JSON-LD instead of extracting from HTML. Breaking change for ActivityPub: the actor URL is now /profile/card.jsonld; remote followers using the old URL will need to re-discover. JSS is still 0.0.x. Fixes JavaScriptSolidServer#282
There was a problem hiding this comment.
Pull request overview
Standardizes newly-generated pod artefacts to JSON-LD files with .jsonld extensions (including the WebID profile), updating WebID URLs and tests accordingly to prevent split discovery chains.
Changes:
- Switch pod bootstrap outputs from
*.ttl/ extensionless profile to*.jsonld(including WebID.../profile/card.jsonld#me). - Update server and IdP pod/WebID construction logic to use the new profile URL.
- Update test expectations to fetch/parse JSON-LD directly and validate new filenames/paths.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/webid/profile.js |
Removes HTML profile shell; returns JSON-LD object and updates prefs/type index links to .jsonld. |
src/handlers/container.js |
Writes profile/card.jsonld, settings/prefs.jsonld, and type indexes as .jsonld; updates generated WebID shape. |
src/server.js |
Updates single-user pod creation to write .jsonld artefacts and adjusts ActivityPub/profile handling paths. |
src/idp/interactions.js |
Updates registration-time WebID construction to .jsonld WebID. |
src/db/index.js |
Updates /db/ ownership check to expect .jsonld WebID. |
test/webid.test.js |
Switches profile assertions from HTML extraction to direct JSON parsing and new content-type/path. |
test/pod.test.js |
Updates pod creation assertions to check .jsonld filenames and new WebID shape. |
test/auth.test.js |
Updates ACL fixture WebID to .jsonld shape. |
Comments suppressed due to low confidence (1)
src/server.js:334
- ActivityPub paths are now switched to
/profile/card.jsonld, but the ActivityPub plugin still hardcodes/profile/card(e.g.,src/ap/index.jsbuilds actor IDs/links as/profile/card#meand registers/profile/card/inbox|outbox|followers|following). With the server-only route moved to/profile/card.jsonld, ActivityPub requests will miss the plugin handlers (and the auth-bypass list below also won’t match the plugin’s endpoints). Update the ActivityPub plugin to use.jsonldconsistently (actorId, profileUrl, route registrations), or add backward-compatible aliases/redirects for/profile/card*while transitioning.
// ActivityPub actor endpoint - dedicated route for /profile/card.jsonld with AP Accept header
// Registered before wildcard routes to take priority
if (activitypubEnabled) {
fastify.route({
method: 'GET',
url: '/profile/card.jsonld',
handler: async (request, reply) => {
const accept = request.headers.accept || '';
const wantsAP = accept.includes('activity+json') ||
accept.includes('ld+json; profile="https://www.w3.org/ns/activitystreams"');
const actorHandler = getActorHandler();
if (wantsAP && actorHandler) {
const actor = actorHandler(request);
return reply
.type('application/activity+json')
.send(actor);
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const webId = `${podUri}profile/card.jsonld#me`; | ||
| const displayName = isRootPod ? 'me' : singleUserName; | ||
|
|
||
| // Check if pod already exists (profile/card is the indicator) | ||
| const profileExists = await storage.exists(`${podPath}profile/card`); | ||
| // Check if pod already exists (profile/card.jsonld is the indicator) | ||
| const profileExists = await storage.exists(`${podPath}profile/card.jsonld`); | ||
|
|
There was a problem hiding this comment.
In single-user mode the existence check uses only profile/card.jsonld as the indicator. If an existing pod was created with the old profile/card (or other legacy layout), this will incorrectly treat it as missing and then overwrite/create root pod files (including /.acl, /settings/*, etc.). Consider checking for both profile/card.jsonld and the legacy profile/card (and/or skipping creation if /.acl or /profile/ already exist), or implement an explicit migration path instead of unconditional creation.
| webId = `${podUri}profile/card.jsonld#me`; | ||
| } else { | ||
| // Path mode: example.com/alice/profile/card#me | ||
| baseUri = `${request.protocol}://${request.hostname}`; |
There was a problem hiding this comment.
The inline documentation still references the legacy WebID shape /alice/profile/card#me (including the subdomain/path mode examples), but the code now generates /profile/card.jsonld#me. Please update these comments to match the new URLs to avoid misleading future changes/debugging.
| webId = `${podUri}profile/card.jsonld#me`; | ||
| } else { | ||
| // Path mode: example.com/alice/profile/card#me | ||
| podUri = `${baseUrl}/${username}/`; |
There was a problem hiding this comment.
The comments in this block still say the WebID follows /profile/card#me (and the subdomain/path examples show /profile/card#me), but the implementation now uses /profile/card.jsonld#me. Update the comments so the documented convention matches the generated WebID.
| const mashlibPaths = ['/mashlib.min.js', '/mash.css', '/841.mashlib.min.js']; | ||
| const apPaths = ['/inbox', '/profile/card/inbox', '/profile/card/outbox', '/profile/card/followers', '/profile/card/following', | ||
| const apPaths = ['/inbox', '/profile/card.jsonld/inbox', '/profile/card.jsonld/outbox', '/profile/card.jsonld/followers', '/profile/card.jsonld/following', | ||
| '/api/v1/apps', '/api/v1/instance', '/api/v1/accounts/verify_credentials', | ||
| '/oauth/authorize', '/oauth/token']; | ||
| // Check if request wants ActivityPub content for profile | ||
| const accept = request.headers.accept || ''; | ||
| const wantsAP = accept.includes('activity+json') || accept.includes('ld+json; profile="https://www.w3.org/ns/activitystreams"'); | ||
| const isProfileAP = activitypubEnabled && wantsAP && (request.url === '/profile/card' || request.url.startsWith('/profile/card?')); | ||
| const isProfileAP = activitypubEnabled && wantsAP && (request.url === '/profile/card.jsonld' || request.url.startsWith('/profile/card.jsonld?')); |
There was a problem hiding this comment.
apPaths now lists /profile/card.jsonld/*, but the ActivityPub plugin currently registers /profile/card/* endpoints (see src/ap/index.js: fastify.post('/profile/card/inbox', ...), etc.). As a result, ActivityPub inbox/outbox/followers/following requests will no longer match this auth-bypass and may start requiring WAC auth unexpectedly. Either update the plugin routes to .jsonld or include both the legacy /profile/card/* and new /profile/card.jsonld/* paths here during transition.
1. ActivityPub routes updated. src/ap/index.js still registered the plugin-level endpoints (/profile/card/inbox, /profile/card/outbox, /profile/card/followers, /profile/card/following) on the old path, plus every actorId/actorUrl/profileUrl in src/ap/routes/* and src/ap/keys.js built the legacy /profile/card URL. Auth-bypass in server.js already listed the .jsonld versions, so without this fix remote AP requests would have started 401'ing. All ten files under src/ap/ now use /profile/card.jsonld consistently. 2. Single-user existence check is backward-compatible. Earlier this PR checked only profile/card.jsonld, so a pod created by an older JSS version (legacy extensionless card) would be treated as missing and the startup hook would overwrite /.acl and /settings/* on boot. Accept either /profile/card.jsonld or /profile/card as the "pod already exists" indicator. 3. Stale comments. src/handlers/container.js and src/idp/interactions.js still documented the WebID as "/alice/profile/card#me"; updated to match the generated `.jsonld#me` URLs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
0f2d017
into
JavaScriptSolidServer:gh-pages
1. XSS-safe inline script. previewConfig is now escaped against the
three sequences that can break out of an inline <script> context:
`</script>` (via `<` → \u003c), and the U+2028 / U+2029 line
separators that JS treats as line terminators. Plain JSON.stringify
doesn't handle these on its own.
2. Client/server validation alignment. The HTML pattern now mirrors
the server's "..-rejected" rule via a negative lookahead, and in
subdomain mode it tightens to alphanumeric + hyphen only (matching
server-side behaviour added below). title text updated to match.
3. Subdomain-mode regex. server.js refuses to route multi-level
subdomains, so `alice.smith` would create a non-addressable pod.
handleRegisterPost now picks /^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])?$/
when request.subdomainsEnabled && request.baseDomain, and uses a
distinct error message so the user knows why dot/underscore is out.
4. Lowercase normalisation. The username field is now coerced to
lowercase as the user types, so the live preview matches what the
server will accept (it rejects uppercase outright).
5. Test coverage. Adds 9 path-mode register cases (accepted: alice,
alice-smith, alice.smith, alice_work; rejected: leading separator,
trailing separator, consecutive dots, uppercase, too-short) and 3
subdomain-mode cases (dash accepted; dot + underscore rejected).
Also picks up the .jsonld pod creation flow from JavaScriptSolidServer#283 transitively.
Three of the four items from #284. Auto-login is deferred to a follow-up PR because it touches OIDC interaction state and deserves its own review. 1. Relaxed username regex. ^[a-z0-9]+$ → ^[a-z0-9]([a-z0-9._-]{1,30}[a-z0-9])?$ Accepts realistic shapes like alice-smith, alice.smith, alice_work. Must still start and end alphanumeric (no leading dot, no trailing separator), no embedded "..". The new pattern is a superset of the old, so no existing user is invalidated. 2. Live WebID preview. Embeds a small JSON config (baseUri, subdomainsEnabled, baseDomain) into the page; a 30-line client-side script renders the WebID + storage URL the user is about to claim, in real time as they type. handleRegisterGet now takes the same `issuer` arg as handleRegisterPost, plumbed via a small `previewContext` helper. 3. Visual polish. Gradient header (matches pilot's onboarding feel), monospace preview block with subtle slate background. All scoped to the register page via `.container.register` so other IDP pages are unchanged. Smoke test: registering alice-smith now succeeds; pod created at /alice-smith/ with the .jsonld layout from #283.
Fixes #282.
Summary
Pod creation now writes `.jsonld` for every RDF artefact:
WebID URL shifts from `https://host/alice/profile/card#me\` to `https://host/alice/profile/card.jsonld#me\`.
Why
JSS is JSON-LD-native (Turtle is opt-in via `--conneg`). Mixing `.ttl` server-side with `.jsonld` client-side produced split discovery chains in the wild — pilot's "Create prefs / TypeIndex" buttons created `.jsonld` files alongside JSS-created `.ttl` files for the same logical resource. Standardising on `.jsonld` everywhere fixes the round-trip.
The HTML profile shell goes away as a server-default. Operators who want a personal-site landing page can still serve their own `index.html` with an embedded `<script type="application/ld+json">` data island — that pattern is unchanged.
Files touched
Breaking changes
Test plan