javascript-solid-server 0.0.32 → 0.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/auth/middleware.js +5 -0
- package/src/auth/solid-oidc.js +6 -3
- package/src/handlers/container.js +15 -15
- package/src/handlers/resource.js +41 -2
- package/src/idp/interactions.js +14 -3
- package/src/ldp/headers.js +1 -1
- package/src/webid/profile.js +9 -4
package/package.json
CHANGED
package/src/auth/middleware.js
CHANGED
|
@@ -28,6 +28,11 @@ export async function authorize(request, reply) {
|
|
|
28
28
|
// Get WebID from token (supports both simple and Solid-OIDC tokens)
|
|
29
29
|
const { webId, error: authError } = await getWebIdFromRequestAsync(request);
|
|
30
30
|
|
|
31
|
+
// Log auth failures for debugging
|
|
32
|
+
if (authError) {
|
|
33
|
+
request.log.warn({ authError, method, urlPath, hasAuth: !!request.headers.authorization }, 'Auth error');
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
// Get effective storage path (includes pod name in subdomain mode)
|
|
32
37
|
const storagePath = getEffectiveUrlPath(request);
|
|
33
38
|
|
package/src/auth/solid-oidc.js
CHANGED
|
@@ -89,16 +89,19 @@ export async function verifySolidOidc(request) {
|
|
|
89
89
|
} catch (err) {
|
|
90
90
|
// Handle specific JWT errors
|
|
91
91
|
if (err.code === 'ERR_JWT_EXPIRED') {
|
|
92
|
+
console.error('Solid-OIDC: Access token expired');
|
|
92
93
|
return { webId: null, error: 'Access token expired' };
|
|
93
94
|
}
|
|
94
95
|
if (err.code === 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') {
|
|
96
|
+
console.error('Solid-OIDC: Invalid token signature');
|
|
95
97
|
return { webId: null, error: 'Invalid token signature' };
|
|
96
98
|
}
|
|
97
99
|
if (err.code === 'ERR_JWKS_NO_MATCHING_KEY') {
|
|
100
|
+
console.error('Solid-OIDC: No matching key found in JWKS');
|
|
98
101
|
return { webId: null, error: 'No matching key found in JWKS' };
|
|
99
102
|
}
|
|
100
103
|
|
|
101
|
-
console.error('Solid-OIDC verification error:', err.message);
|
|
104
|
+
console.error('Solid-OIDC verification error:', err.code, err.message);
|
|
102
105
|
return { webId: null, error: 'Token verification failed' };
|
|
103
106
|
}
|
|
104
107
|
}
|
|
@@ -177,8 +180,8 @@ async function verifyDpopProof(dpopProof, request, accessToken) {
|
|
|
177
180
|
return { thumbprint, error: null };
|
|
178
181
|
|
|
179
182
|
} catch (err) {
|
|
180
|
-
console.error('DPoP verification error:', err.message);
|
|
181
|
-
return { thumbprint: null, error: 'Invalid DPoP proof' };
|
|
183
|
+
console.error('DPoP verification error:', err.code, err.message);
|
|
184
|
+
return { thumbprint: null, error: 'Invalid DPoP proof: ' + err.message };
|
|
182
185
|
}
|
|
183
186
|
}
|
|
184
187
|
|
|
@@ -125,35 +125,35 @@ export async function handlePost(request, reply) {
|
|
|
125
125
|
* Create pod directory structure (reusable for registration)
|
|
126
126
|
* @param {string} name - Pod name (username)
|
|
127
127
|
* @param {string} webId - User's WebID URI
|
|
128
|
-
* @param {string}
|
|
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
|
|
129
130
|
*/
|
|
130
|
-
export async function createPodStructure(name, webId,
|
|
131
|
+
export async function createPodStructure(name, webId, podUri, issuer) {
|
|
131
132
|
const podPath = `/${name}/`;
|
|
132
|
-
const podUri = `${baseUrl}/${name}/`;
|
|
133
|
-
const issuer = baseUrl + '/';
|
|
134
133
|
|
|
135
134
|
// Create pod directory structure
|
|
135
|
+
// Uses 'Settings' (capital S) for mashlib compatibility
|
|
136
136
|
await storage.createContainer(podPath);
|
|
137
137
|
await storage.createContainer(`${podPath}inbox/`);
|
|
138
138
|
await storage.createContainer(`${podPath}public/`);
|
|
139
139
|
await storage.createContainer(`${podPath}private/`);
|
|
140
|
-
await storage.createContainer(`${podPath}
|
|
140
|
+
await storage.createContainer(`${podPath}Settings/`);
|
|
141
141
|
await storage.createContainer(`${podPath}profile/`);
|
|
142
142
|
|
|
143
143
|
// Generate and write WebID profile at /profile/card (standard Solid location)
|
|
144
144
|
const profileHtml = generateProfile({ webId, name, podUri, issuer });
|
|
145
145
|
await storage.write(`${podPath}profile/card`, profileHtml);
|
|
146
146
|
|
|
147
|
-
// Generate and write preferences
|
|
147
|
+
// Generate and write preferences (mashlib-compatible paths)
|
|
148
148
|
const prefs = generatePreferences({ webId, podUri });
|
|
149
|
-
await storage.write(`${podPath}
|
|
149
|
+
await storage.write(`${podPath}Settings/Preferences.ttl`, serialize(prefs));
|
|
150
150
|
|
|
151
|
-
// Generate and write type indexes
|
|
152
|
-
const publicTypeIndex = generateTypeIndex(`${podUri}
|
|
153
|
-
await storage.write(`${podPath}
|
|
151
|
+
// Generate and write type indexes with .ttl extension for mashlib
|
|
152
|
+
const publicTypeIndex = generateTypeIndex(`${podUri}Settings/publicTypeIndex.ttl`);
|
|
153
|
+
await storage.write(`${podPath}Settings/publicTypeIndex.ttl`, serialize(publicTypeIndex));
|
|
154
154
|
|
|
155
|
-
const privateTypeIndex = generateTypeIndex(`${podUri}
|
|
156
|
-
await storage.write(`${podPath}
|
|
155
|
+
const privateTypeIndex = generateTypeIndex(`${podUri}Settings/privateTypeIndex.ttl`);
|
|
156
|
+
await storage.write(`${podPath}Settings/privateTypeIndex.ttl`, serialize(privateTypeIndex));
|
|
157
157
|
|
|
158
158
|
// Create default ACL files
|
|
159
159
|
// Pod root: owner full control, public read
|
|
@@ -165,8 +165,8 @@ export async function createPodStructure(name, webId, baseUrl) {
|
|
|
165
165
|
await storage.write(`${podPath}private/.acl`, serializeAcl(privateAcl));
|
|
166
166
|
|
|
167
167
|
// Settings folder: owner only
|
|
168
|
-
const settingsAcl = generatePrivateAcl(`${podUri}
|
|
169
|
-
await storage.write(`${podPath}
|
|
168
|
+
const settingsAcl = generatePrivateAcl(`${podUri}Settings/`, webId);
|
|
169
|
+
await storage.write(`${podPath}Settings/.acl`, serializeAcl(settingsAcl));
|
|
170
170
|
|
|
171
171
|
// Inbox: owner full, public append
|
|
172
172
|
const inboxAcl = generateInboxAcl(`${podUri}inbox/`, webId);
|
|
@@ -253,7 +253,7 @@ export async function handleCreatePod(request, reply) {
|
|
|
253
253
|
|
|
254
254
|
try {
|
|
255
255
|
// Use shared pod creation function
|
|
256
|
-
await createPodStructure(name, webId,
|
|
256
|
+
await createPodStructure(name, webId, podUri, issuer);
|
|
257
257
|
} catch (err) {
|
|
258
258
|
console.error('Pod creation error:', err);
|
|
259
259
|
// Cleanup on failure
|
package/src/handlers/resource.js
CHANGED
|
@@ -146,6 +146,42 @@ export async function handleGet(request, reply) {
|
|
|
146
146
|
return reply.type('text/html').send(html);
|
|
147
147
|
}
|
|
148
148
|
|
|
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
|
+
|
|
149
185
|
const headers = getAllHeaders({
|
|
150
186
|
isContainer: true,
|
|
151
187
|
etag: stats.etag,
|
|
@@ -196,7 +232,9 @@ export async function handleGet(request, reply) {
|
|
|
196
232
|
if (connegEnabled) {
|
|
197
233
|
const contentStr = content.toString();
|
|
198
234
|
const acceptHeader = request.headers.accept || '';
|
|
199
|
-
|
|
235
|
+
// Serve Turtle if: URL ends with .ttl OR Accept header requests it
|
|
236
|
+
const wantsTurtle = urlPath.endsWith('.ttl') ||
|
|
237
|
+
acceptHeader.includes('text/turtle') ||
|
|
200
238
|
acceptHeader.includes('text/n3') ||
|
|
201
239
|
acceptHeader.includes('application/n-triples');
|
|
202
240
|
|
|
@@ -233,7 +271,8 @@ export async function handleGet(request, reply) {
|
|
|
233
271
|
// Plain JSON-LD file
|
|
234
272
|
try {
|
|
235
273
|
const jsonLd = JSON.parse(contentStr);
|
|
236
|
-
|
|
274
|
+
// Use Turtle if URL ends with .ttl, otherwise use Accept header preference
|
|
275
|
+
const targetType = wantsTurtle ? 'text/turtle' : selectContentType(acceptHeader, connegEnabled);
|
|
237
276
|
const { content: outputContent, contentType: outputType } = await fromJsonLd(
|
|
238
277
|
jsonLd,
|
|
239
278
|
targetType,
|
package/src/idp/interactions.js
CHANGED
|
@@ -355,9 +355,20 @@ export async function handleRegisterPost(request, reply, issuer) {
|
|
|
355
355
|
|
|
356
356
|
try {
|
|
357
357
|
// Build URLs - WebID follows standard Solid convention: /profile/card#me
|
|
358
|
+
const subdomainsEnabled = request.subdomainsEnabled;
|
|
359
|
+
const baseDomain = request.baseDomain;
|
|
358
360
|
const baseUrl = issuer.endsWith('/') ? issuer.slice(0, -1) : issuer;
|
|
359
|
-
|
|
360
|
-
|
|
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
|
+
}
|
|
361
372
|
|
|
362
373
|
// Check if pod already exists
|
|
363
374
|
const podPath = `${username}/`;
|
|
@@ -367,7 +378,7 @@ export async function handleRegisterPost(request, reply, issuer) {
|
|
|
367
378
|
}
|
|
368
379
|
|
|
369
380
|
// Create pod structure
|
|
370
|
-
await createPodStructure(username, webId,
|
|
381
|
+
await createPodStructure(username, webId, podUri, issuer);
|
|
371
382
|
|
|
372
383
|
// Create account
|
|
373
384
|
await createAccount({
|
package/src/ldp/headers.js
CHANGED
|
@@ -90,7 +90,7 @@ export function getCorsHeaders(origin) {
|
|
|
90
90
|
return {
|
|
91
91
|
'Access-Control-Allow-Origin': origin || '*',
|
|
92
92
|
'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',
|
|
94
94
|
'Access-Control-Expose-Headers': 'Accept-Patch, Accept-Post, Allow, Content-Type, ETag, Link, Location, Updates-Via, WAC-Allow',
|
|
95
95
|
'Access-Control-Allow-Credentials': 'true',
|
|
96
96
|
'Access-Control-Max-Age': '86400'
|
package/src/webid/profile.js
CHANGED
|
@@ -34,6 +34,8 @@ export function generateProfileJsonLd({ webId, name, podUri, issuer }) {
|
|
|
34
34
|
'storage': { '@id': 'pim:storage', '@type': '@id' },
|
|
35
35
|
'oidcIssuer': { '@id': 'solid:oidcIssuer', '@type': '@id' },
|
|
36
36
|
'preferencesFile': { '@id': 'pim:preferencesFile', '@type': '@id' },
|
|
37
|
+
'publicTypeIndex': { '@id': 'solid:publicTypeIndex', '@type': '@id' },
|
|
38
|
+
'privateTypeIndex': { '@id': 'solid:privateTypeIndex', '@type': '@id' },
|
|
37
39
|
'mainEntityOfPage': { '@id': 'schema:mainEntityOfPage', '@type': '@id' }
|
|
38
40
|
},
|
|
39
41
|
'@id': webId,
|
|
@@ -43,7 +45,9 @@ export function generateProfileJsonLd({ webId, name, podUri, issuer }) {
|
|
|
43
45
|
'inbox': `${pod}inbox/`,
|
|
44
46
|
'storage': pod,
|
|
45
47
|
'oidcIssuer': issuer,
|
|
46
|
-
'preferencesFile': `${pod}
|
|
48
|
+
'preferencesFile': `${pod}Settings/Preferences.ttl`,
|
|
49
|
+
'publicTypeIndex': `${pod}Settings/publicTypeIndex.ttl`,
|
|
50
|
+
'privateTypeIndex': `${pod}Settings/privateTypeIndex.ttl`
|
|
47
51
|
};
|
|
48
52
|
}
|
|
49
53
|
|
|
@@ -129,6 +133,7 @@ function escapeHtml(str) {
|
|
|
129
133
|
|
|
130
134
|
/**
|
|
131
135
|
* Generate preferences file as JSON-LD
|
|
136
|
+
* Uses mashlib-compatible paths (Settings/Preferences.ttl)
|
|
132
137
|
* @param {object} options
|
|
133
138
|
* @param {string} options.webId - Full WebID URI
|
|
134
139
|
* @param {string} options.podUri - Pod root URI
|
|
@@ -144,9 +149,9 @@ export function generatePreferences({ webId, podUri }) {
|
|
|
144
149
|
'publicTypeIndex': { '@id': 'solid:publicTypeIndex', '@type': '@id' },
|
|
145
150
|
'privateTypeIndex': { '@id': 'solid:privateTypeIndex', '@type': '@id' }
|
|
146
151
|
},
|
|
147
|
-
'@id': `${pod}
|
|
148
|
-
'publicTypeIndex': `${pod}
|
|
149
|
-
'privateTypeIndex': `${pod}
|
|
152
|
+
'@id': `${pod}Settings/Preferences.ttl`,
|
|
153
|
+
'publicTypeIndex': `${pod}Settings/publicTypeIndex.ttl`,
|
|
154
|
+
'privateTypeIndex': `${pod}Settings/privateTypeIndex.ttl`
|
|
150
155
|
};
|
|
151
156
|
}
|
|
152
157
|
|