Skip to content

Commit b8e566e

Browse files
Fix subdomain mode and content negotiation issues
- Add content negotiation for container listings (return Turtle when Accept: text/turtle) - Fix subdomain URL generation in pod creation and registration - Add DPoP to CORS Access-Control-Allow-Headers for Solid-OIDC - Serve .ttl extension files as Turtle regardless of Accept header Fixes JavaScriptSolidServer#19
1 parent 83bffe7 commit b8e566e

4 files changed

Lines changed: 60 additions & 11 deletions

File tree

src/handlers/container.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,11 @@ export async function handlePost(request, reply) {
125125
* Create pod directory structure (reusable for registration)
126126
* @param {string} name - Pod name (username)
127127
* @param {string} webId - User's WebID URI
128-
* @param {string} baseUrl - Base URL (without trailing slash)
128+
* @param {string} podUri - Pod root URI (e.g., https://alice.example.com/ or https://example.com/alice/)
129+
* @param {string} issuer - OIDC issuer URI
129130
*/
130-
export async function createPodStructure(name, webId, baseUrl) {
131+
export async function createPodStructure(name, webId, podUri, issuer) {
131132
const podPath = `/${name}/`;
132-
const podUri = `${baseUrl}/${name}/`;
133-
const issuer = baseUrl + '/';
134133

135134
// Create pod directory structure
136135
await storage.createContainer(podPath);
@@ -253,7 +252,7 @@ export async function handleCreatePod(request, reply) {
253252

254253
try {
255254
// Use shared pod creation function
256-
await createPodStructure(name, webId, baseUri);
255+
await createPodStructure(name, webId, podUri, issuer);
257256
} catch (err) {
258257
console.error('Pod creation error:', err);
259258
// Cleanup on failure

src/handlers/resource.js

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,42 @@ export async function handleGet(request, reply) {
146146
return reply.type('text/html').send(html);
147147
}
148148

149+
// Check if Turtle/N3 format is requested via content negotiation
150+
const acceptHeader = request.headers.accept || '';
151+
const wantsTurtle = connegEnabled && (
152+
acceptHeader.includes('text/turtle') ||
153+
acceptHeader.includes('text/n3') ||
154+
acceptHeader.includes('application/n-triples')
155+
);
156+
157+
if (wantsTurtle) {
158+
// Convert container JSON-LD to Turtle
159+
try {
160+
const { content: turtleContent } = await fromJsonLd(
161+
jsonLd,
162+
'text/turtle',
163+
resourceUrl,
164+
true
165+
);
166+
167+
const headers = getAllHeaders({
168+
isContainer: true,
169+
etag: stats.etag,
170+
contentType: 'text/turtle',
171+
origin,
172+
resourceUrl,
173+
connegEnabled
174+
});
175+
headers['Vary'] = 'Accept';
176+
177+
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
178+
return reply.send(turtleContent);
179+
} catch (err) {
180+
// Fall through to JSON-LD if conversion fails
181+
console.error('Failed to convert container to Turtle:', err.message);
182+
}
183+
}
184+
149185
const headers = getAllHeaders({
150186
isContainer: true,
151187
etag: stats.etag,
@@ -196,7 +232,9 @@ export async function handleGet(request, reply) {
196232
if (connegEnabled) {
197233
const contentStr = content.toString();
198234
const acceptHeader = request.headers.accept || '';
199-
const wantsTurtle = acceptHeader.includes('text/turtle') ||
235+
// Serve Turtle if: URL ends with .ttl OR Accept header requests it
236+
const wantsTurtle = urlPath.endsWith('.ttl') ||
237+
acceptHeader.includes('text/turtle') ||
200238
acceptHeader.includes('text/n3') ||
201239
acceptHeader.includes('application/n-triples');
202240

@@ -233,7 +271,8 @@ export async function handleGet(request, reply) {
233271
// Plain JSON-LD file
234272
try {
235273
const jsonLd = JSON.parse(contentStr);
236-
const targetType = selectContentType(acceptHeader, connegEnabled);
274+
// Use Turtle if URL ends with .ttl, otherwise use Accept header preference
275+
const targetType = wantsTurtle ? 'text/turtle' : selectContentType(acceptHeader, connegEnabled);
237276
const { content: outputContent, contentType: outputType } = await fromJsonLd(
238277
jsonLd,
239278
targetType,

src/idp/interactions.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -355,9 +355,20 @@ export async function handleRegisterPost(request, reply, issuer) {
355355

356356
try {
357357
// Build URLs - WebID follows standard Solid convention: /profile/card#me
358+
const subdomainsEnabled = request.subdomainsEnabled;
359+
const baseDomain = request.baseDomain;
358360
const baseUrl = issuer.endsWith('/') ? issuer.slice(0, -1) : issuer;
359-
const podUri = `${baseUrl}/${username}/`;
360-
const webId = `${podUri}profile/card#me`;
361+
362+
let podUri, webId;
363+
if (subdomainsEnabled && baseDomain) {
364+
// Subdomain mode: alice.example.com/profile/card#me
365+
podUri = `${request.protocol}://${username}.${baseDomain}/`;
366+
webId = `${podUri}profile/card#me`;
367+
} else {
368+
// Path mode: example.com/alice/profile/card#me
369+
podUri = `${baseUrl}/${username}/`;
370+
webId = `${podUri}profile/card#me`;
371+
}
361372

362373
// Check if pod already exists
363374
const podPath = `${username}/`;
@@ -367,7 +378,7 @@ export async function handleRegisterPost(request, reply, issuer) {
367378
}
368379

369380
// Create pod structure
370-
await createPodStructure(username, webId, baseUrl);
381+
await createPodStructure(username, webId, podUri, issuer);
371382

372383
// Create account
373384
await createAccount({

src/ldp/headers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export function getCorsHeaders(origin) {
9090
return {
9191
'Access-Control-Allow-Origin': origin || '*',
9292
'Access-Control-Allow-Methods': 'GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS',
93-
'Access-Control-Allow-Headers': 'Accept, Authorization, Content-Type, If-Match, If-None-Match, Link, Slug, Origin',
93+
'Access-Control-Allow-Headers': 'Accept, Authorization, Content-Type, DPoP, If-Match, If-None-Match, Link, Slug, Origin',
9494
'Access-Control-Expose-Headers': 'Accept-Patch, Accept-Post, Allow, Content-Type, ETag, Link, Location, Updates-Via, WAC-Allow',
9595
'Access-Control-Allow-Credentials': 'true',
9696
'Access-Control-Max-Age': '86400'

0 commit comments

Comments
 (0)