Skip to content

Use .jsonld for all generated pod files (#282)#283

Merged
melvincarvalho merged 2 commits into
JavaScriptSolidServer:gh-pagesfrom
melvincarvalho:issue-282-pod-files-jsonld
Apr 21, 2026
Merged

Use .jsonld for all generated pod files (#282)#283
melvincarvalho merged 2 commits into
JavaScriptSolidServer:gh-pagesfrom
melvincarvalho:issue-282-pod-files-jsonld

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Fixes #282.

Summary

Pod creation now writes `.jsonld` for every RDF artefact:

Before After
`profile/card` (HTML wrapper) `profile/card.jsonld` (plain JSON-LD)
`settings/Preferences.ttl` `settings/prefs.jsonld`
`settings/publicTypeIndex.ttl` `settings/publicTypeIndex.jsonld`
`settings/privateTypeIndex.ttl` `settings/privateTypeIndex.jsonld`

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

  • `src/webid/profile.js` — `generateProfile()` now returns the JSON-LD object directly; HTML wrapper deleted. Predicate values for `preferencesFile` / `publicTypeIndex` / `privateTypeIndex` updated to `.jsonld` paths.
  • `src/handlers/container.js` (`POST /.pods`) — writes `.jsonld` filenames, builds WebID with `.jsonld` extension.
  • `src/server.js` — same updates in `createRootPodStructure` (single-user root pod). ActivityPub actor route + auth-bypass path list moved to `/profile/card.jsonld` and friends.
  • `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` — assertions updated to the new paths; `webid.test.js` parses JSON-LD directly via `res.json()` instead of `extractJsonLdFromHtml`.

Breaking changes

  • WebID URL changes (`/profile/card` → `/profile/card.jsonld`). Tokens issued before the upgrade reference the old URL and become invalid after; users will need to sign in again.
  • ActivityPub actor URL changes. Remote followers using the old URL will need to re-discover. JSS is still 0.0.x — acceptable cost.
  • Existing pods still work as-is; this only affects newly created pods. Operators with existing `.ttl` files can rename + update WebID predicates manually (or leave them — the discovery chain still resolves whatever the WebID points at).

Test plan

  • Full suite: 379/379 pass (no count change).
  • `webid.test.js` updated to parse JSON-LD directly; covers content-type, `@id`, `foaf:name`, `oidcIssuer`, `pim:storage`, `ldp:inbox`, `mainEntityOfPage`, LDP/CORS headers.
  • `pod.test.js` updated to check the new `.jsonld` filenames and the new `webId` shape from `POST /.pods`.
  • `auth.test.js` updated for the new agent WebID shape in ACL fixtures.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.js builds actor IDs/links as /profile/card#me and 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 .jsonld consistently (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.

Comment thread src/server.js
Comment on lines +560 to 565
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`);

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread src/handlers/container.js Outdated
Comment on lines 275 to 289
webId = `${podUri}profile/card.jsonld#me`;
} else {
// Path mode: example.com/alice/profile/card#me
baseUri = `${request.protocol}://${request.hostname}`;

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread src/idp/interactions.js Outdated
Comment on lines 408 to 420
webId = `${podUri}profile/card.jsonld#me`;
} else {
// Path mode: example.com/alice/profile/card#me
podUri = `${baseUrl}/${username}/`;

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread src/server.js
Comment on lines 428 to +435
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?'));

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@melvincarvalho
melvincarvalho merged commit 0f2d017 into JavaScriptSolidServer:gh-pages Apr 21, 2026
4 checks passed
melvincarvalho added a commit to melvincarvalho/jss that referenced this pull request Apr 22, 2026
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.
melvincarvalho added a commit that referenced this pull request Apr 22, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

createPodStructure should write .jsonld (not .ttl) for prefs and TypeIndexes

2 participants