javascript-solid-server 0.0.27 → 0.0.29
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/handlers/container.js +13 -7
- package/src/handlers/resource.js +49 -9
- package/src/idp/interactions.js +2 -2
- package/src/webid/profile.js +41 -19
package/package.json
CHANGED
|
@@ -138,10 +138,11 @@ export async function createPodStructure(name, webId, baseUrl) {
|
|
|
138
138
|
await storage.createContainer(`${podPath}public/`);
|
|
139
139
|
await storage.createContainer(`${podPath}private/`);
|
|
140
140
|
await storage.createContainer(`${podPath}settings/`);
|
|
141
|
+
await storage.createContainer(`${podPath}profile/`);
|
|
141
142
|
|
|
142
|
-
// Generate and write WebID profile
|
|
143
|
+
// Generate and write WebID profile at /profile/card (standard Solid location)
|
|
143
144
|
const profileHtml = generateProfile({ webId, name, podUri, issuer });
|
|
144
|
-
await storage.write(`${podPath}
|
|
145
|
+
await storage.write(`${podPath}profile/card`, profileHtml);
|
|
145
146
|
|
|
146
147
|
// Generate and write preferences
|
|
147
148
|
const prefs = generatePreferences({ webId, podUri });
|
|
@@ -175,6 +176,11 @@ export async function createPodStructure(name, webId, baseUrl) {
|
|
|
175
176
|
const publicAcl = generatePublicFolderAcl(`${podUri}public/`, webId);
|
|
176
177
|
await storage.write(`${podPath}public/.acl`, serializeAcl(publicAcl));
|
|
177
178
|
|
|
179
|
+
// Profile folder: owner full, public read (with inheritance)
|
|
180
|
+
// Profile documents must be publicly readable for WebID verification
|
|
181
|
+
const profileAcl = generatePublicFolderAcl(`${podUri}profile/`, webId);
|
|
182
|
+
await storage.write(`${podPath}profile/.acl`, serializeAcl(profileAcl));
|
|
183
|
+
|
|
178
184
|
return { podPath, podUri };
|
|
179
185
|
}
|
|
180
186
|
|
|
@@ -224,22 +230,22 @@ export async function handleCreatePod(request, reply) {
|
|
|
224
230
|
}
|
|
225
231
|
|
|
226
232
|
// Build URIs
|
|
227
|
-
// WebID
|
|
233
|
+
// WebID follows standard Solid convention: /alice/profile/card#me
|
|
228
234
|
const subdomainsEnabled = request.subdomainsEnabled;
|
|
229
235
|
const baseDomain = request.baseDomain;
|
|
230
236
|
|
|
231
237
|
let baseUri, podUri, webId;
|
|
232
238
|
if (subdomainsEnabled && baseDomain) {
|
|
233
|
-
// Subdomain mode: alice.example.com/
|
|
239
|
+
// Subdomain mode: alice.example.com/profile/card#me
|
|
234
240
|
const podHost = `${name}.${baseDomain}`;
|
|
235
241
|
baseUri = `${request.protocol}://${baseDomain}`;
|
|
236
242
|
podUri = `${request.protocol}://${podHost}/`;
|
|
237
|
-
webId = `${podUri}#me`;
|
|
243
|
+
webId = `${podUri}profile/card#me`;
|
|
238
244
|
} else {
|
|
239
|
-
// Path mode: example.com/alice/
|
|
245
|
+
// Path mode: example.com/alice/profile/card#me
|
|
240
246
|
baseUri = `${request.protocol}://${request.hostname}`;
|
|
241
247
|
podUri = `${baseUri}${podPath}`;
|
|
242
|
-
webId = `${podUri}#me`;
|
|
248
|
+
webId = `${podUri}profile/card#me`;
|
|
243
249
|
}
|
|
244
250
|
|
|
245
251
|
// Issuer needs trailing slash for CTH compatibility
|
package/src/handlers/resource.js
CHANGED
|
@@ -495,20 +495,53 @@ export async function handlePatch(request, reply) {
|
|
|
495
495
|
|
|
496
496
|
// Read existing content or start with empty JSON-LD document
|
|
497
497
|
let document;
|
|
498
|
+
let htmlWrapper = null; // Track HTML wrapper for data island re-embedding
|
|
499
|
+
|
|
498
500
|
if (resourceExists) {
|
|
499
501
|
const existingContent = await storage.read(storagePath);
|
|
500
502
|
if (existingContent === null) {
|
|
501
503
|
return reply.code(500).send({ error: 'Read error' });
|
|
502
504
|
}
|
|
503
505
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
506
|
+
const contentStr = existingContent.toString();
|
|
507
|
+
|
|
508
|
+
// Check if this is HTML with embedded JSON-LD data island
|
|
509
|
+
if (contentStr.trimStart().startsWith('<!DOCTYPE') || contentStr.trimStart().startsWith('<html')) {
|
|
510
|
+
// Extract JSON-LD from <script type="application/ld+json"> tag
|
|
511
|
+
const jsonLdMatch = contentStr.match(/<script\s+type=["']application\/ld\+json["']\s*>([\s\S]*?)<\/script>/i);
|
|
512
|
+
|
|
513
|
+
if (!jsonLdMatch) {
|
|
514
|
+
return reply.code(409).send({
|
|
515
|
+
error: 'Conflict',
|
|
516
|
+
message: 'HTML document does not contain a JSON-LD data island'
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
document = JSON.parse(jsonLdMatch[1]);
|
|
522
|
+
// Save the HTML parts for re-embedding after patch
|
|
523
|
+
const jsonLdStart = contentStr.indexOf(jsonLdMatch[0]) + jsonLdMatch[0].indexOf('>') + 1;
|
|
524
|
+
const jsonLdEnd = jsonLdStart + jsonLdMatch[1].length;
|
|
525
|
+
htmlWrapper = {
|
|
526
|
+
before: contentStr.substring(0, jsonLdStart),
|
|
527
|
+
after: contentStr.substring(jsonLdEnd)
|
|
528
|
+
};
|
|
529
|
+
} catch (e) {
|
|
530
|
+
return reply.code(409).send({
|
|
531
|
+
error: 'Conflict',
|
|
532
|
+
message: 'HTML data island contains invalid JSON-LD'
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
// Parse as plain JSON-LD
|
|
537
|
+
try {
|
|
538
|
+
document = JSON.parse(contentStr);
|
|
539
|
+
} catch (e) {
|
|
540
|
+
return reply.code(409).send({
|
|
541
|
+
error: 'Conflict',
|
|
542
|
+
message: 'Resource is not valid JSON-LD and cannot be patched'
|
|
543
|
+
});
|
|
544
|
+
}
|
|
512
545
|
}
|
|
513
546
|
} else {
|
|
514
547
|
// Create empty JSON-LD document for new resource
|
|
@@ -568,7 +601,14 @@ export async function handlePatch(request, reply) {
|
|
|
568
601
|
}
|
|
569
602
|
|
|
570
603
|
// Write updated document
|
|
571
|
-
|
|
604
|
+
let updatedContent;
|
|
605
|
+
if (htmlWrapper) {
|
|
606
|
+
// Re-embed JSON-LD into HTML wrapper
|
|
607
|
+
const jsonLdStr = JSON.stringify(updatedDocument, null, 2);
|
|
608
|
+
updatedContent = htmlWrapper.before + '\n' + jsonLdStr + '\n ' + htmlWrapper.after;
|
|
609
|
+
} else {
|
|
610
|
+
updatedContent = JSON.stringify(updatedDocument, null, 2);
|
|
611
|
+
}
|
|
572
612
|
const success = await storage.write(storagePath, Buffer.from(updatedContent));
|
|
573
613
|
|
|
574
614
|
if (!success) {
|
package/src/idp/interactions.js
CHANGED
|
@@ -354,10 +354,10 @@ export async function handleRegisterPost(request, reply, issuer) {
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
try {
|
|
357
|
-
// Build URLs
|
|
357
|
+
// Build URLs - WebID follows standard Solid convention: /profile/card#me
|
|
358
358
|
const baseUrl = issuer.endsWith('/') ? issuer.slice(0, -1) : issuer;
|
|
359
359
|
const podUri = `${baseUrl}/${username}/`;
|
|
360
|
-
const webId = `${podUri}#me`;
|
|
360
|
+
const webId = `${podUri}profile/card#me`;
|
|
361
361
|
|
|
362
362
|
// Check if pod already exists
|
|
363
363
|
const podPath = `${username}/`;
|
package/src/webid/profile.js
CHANGED
|
@@ -56,17 +56,17 @@ export function generateProfileJsonLd({ webId, name, podUri, issuer }) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* Generate HTML profile with embedded JSON-LD
|
|
59
|
+
* Generate HTML profile with embedded JSON-LD data island
|
|
60
|
+
* The page uses mashlib + solidos-lite to render the profile from the data island
|
|
60
61
|
* @param {object} options
|
|
61
62
|
* @param {string} options.webId - Full WebID URI
|
|
62
63
|
* @param {string} options.name - Display name
|
|
63
64
|
* @param {string} options.podUri - Pod root URI
|
|
64
65
|
* @param {string} options.issuer - OIDC issuer URI
|
|
65
|
-
* @returns {string} HTML document with JSON-LD
|
|
66
|
+
* @returns {string} HTML document with JSON-LD data island
|
|
66
67
|
*/
|
|
67
68
|
export function generateProfile({ webId, name, podUri, issuer }) {
|
|
68
69
|
const jsonLd = generateProfileJsonLd({ webId, name, podUri, issuer });
|
|
69
|
-
const pod = podUri.endsWith('/') ? podUri : podUri + '/';
|
|
70
70
|
|
|
71
71
|
return `<!DOCTYPE html>
|
|
72
72
|
<html lang="en">
|
|
@@ -74,30 +74,52 @@ export function generateProfile({ webId, name, podUri, issuer }) {
|
|
|
74
74
|
<meta charset="utf-8">
|
|
75
75
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
76
76
|
<title>${escapeHtml(name)}'s Profile</title>
|
|
77
|
+
<link rel="stylesheet" href="https://javascriptsolidserver.github.io/mashlib-jss/dist/mash.css">
|
|
77
78
|
<script type="application/ld+json">
|
|
78
79
|
${JSON.stringify(jsonLd, null, 2)}
|
|
79
80
|
</script>
|
|
80
81
|
<style>
|
|
81
|
-
body { font-family: system-ui, sans-serif;
|
|
82
|
-
|
|
83
|
-
.card { background: #f5f5f5; padding: 1.5rem; border-radius: 8px; }
|
|
84
|
-
dt { font-weight: bold; margin-top: 1rem; }
|
|
85
|
-
dd { margin-left: 0; color: #666; }
|
|
86
|
-
a { color: #7c4dff; }
|
|
82
|
+
body { margin: 0; font-family: system-ui, sans-serif; }
|
|
83
|
+
.loading { padding: 2rem; text-align: center; color: #666; }
|
|
87
84
|
</style>
|
|
88
85
|
</head>
|
|
89
86
|
<body>
|
|
90
|
-
<div class="
|
|
91
|
-
<
|
|
92
|
-
<
|
|
93
|
-
<dt>WebID</dt>
|
|
94
|
-
<dd><a href="${escapeHtml(webId)}">${escapeHtml(webId)}</a></dd>
|
|
95
|
-
<dt>Storage</dt>
|
|
96
|
-
<dd><a href="${escapeHtml(pod)}">${escapeHtml(pod)}</a></dd>
|
|
97
|
-
<dt>Inbox</dt>
|
|
98
|
-
<dd><a href="${escapeHtml(pod)}inbox/">${escapeHtml(pod)}inbox/</a></dd>
|
|
99
|
-
</dl>
|
|
87
|
+
<div class="TabulatorOutline" id="DummyUUID" role="main">
|
|
88
|
+
<table id="outline"></table>
|
|
89
|
+
<div id="GlobalDashboard"></div>
|
|
100
90
|
</div>
|
|
91
|
+
<div class="loading" id="loading">Loading profile...</div>
|
|
92
|
+
|
|
93
|
+
<script src="https://javascriptsolidserver.github.io/mashlib-jss/dist/mashlib.min.js"></script>
|
|
94
|
+
<script src="https://cdn.jsdelivr.net/npm/solidos-lite/solidos-lite.js"></script>
|
|
95
|
+
<script>
|
|
96
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
97
|
+
const loadingEl = document.getElementById('loading');
|
|
98
|
+
|
|
99
|
+
// Initialize solidos-lite to handle data islands
|
|
100
|
+
const success = SolidOSLite.init({ verbose: false });
|
|
101
|
+
if (!success) {
|
|
102
|
+
loadingEl.textContent = 'Failed to initialize. Please try refreshing.';
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Parse data islands into the RDF store
|
|
107
|
+
SolidOSLite.parseAllIslands();
|
|
108
|
+
|
|
109
|
+
// Mark this document as already fetched
|
|
110
|
+
const pageBase = window.location.href.split('?')[0].split('#')[0];
|
|
111
|
+
const fetcher = SolidLogic.store.fetcher;
|
|
112
|
+
fetcher.requested[pageBase] = 'done';
|
|
113
|
+
fetcher.requested[pageBase.replace(/\\/$/, '')] = 'done';
|
|
114
|
+
|
|
115
|
+
// Navigate to #me
|
|
116
|
+
const subject = $rdf.sym(pageBase + '#me');
|
|
117
|
+
const outliner = panes.getOutliner(document);
|
|
118
|
+
outliner.GotoSubject(subject, true, undefined, true, undefined);
|
|
119
|
+
|
|
120
|
+
loadingEl.style.display = 'none';
|
|
121
|
+
});
|
|
122
|
+
</script>
|
|
101
123
|
</body>
|
|
102
124
|
</html>`;
|
|
103
125
|
}
|