javascript-solid-server 0.0.67 → 0.0.69
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/.claude/settings.local.json +3 -1
- package/package.json +1 -1
- package/src/auth/middleware.js +231 -5
- package/src/server.js +2 -2
package/package.json
CHANGED
package/src/auth/middleware.js
CHANGED
|
@@ -93,26 +93,36 @@ function getParentPath(path) {
|
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
95
|
* Handle unauthorized request
|
|
96
|
+
* @param {object} request - Fastify request
|
|
96
97
|
* @param {object} reply - Fastify reply
|
|
97
98
|
* @param {boolean} isAuthenticated - Whether user is authenticated
|
|
98
99
|
* @param {string} wacAllow - WAC-Allow header value
|
|
99
100
|
* @param {string|null} authError - Authentication error message (for DPoP failures)
|
|
100
101
|
* @param {string|null} issuer - IdP issuer URL for WWW-Authenticate header
|
|
101
102
|
*/
|
|
102
|
-
export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError = null, issuer = null) {
|
|
103
|
+
export function handleUnauthorized(request, reply, isAuthenticated, wacAllow, authError = null, issuer = null) {
|
|
103
104
|
reply.header('WAC-Allow', wacAllow);
|
|
104
105
|
|
|
106
|
+
const statusCode = isAuthenticated ? 403 : 401;
|
|
107
|
+
const realm = issuer || 'Solid';
|
|
108
|
+
|
|
105
109
|
if (!isAuthenticated) {
|
|
106
|
-
// Not authenticated - return 401 with WWW-Authenticate header
|
|
107
|
-
// Solid-OIDC requires DPoP authentication
|
|
108
|
-
const realm = issuer || 'Solid';
|
|
109
110
|
reply.header('WWW-Authenticate', `DPoP realm="${realm}", Bearer realm="${realm}"`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check if browser wants HTML
|
|
114
|
+
const accept = request.headers.accept || '';
|
|
115
|
+
if (accept.includes('text/html')) {
|
|
116
|
+
return reply.code(statusCode).type('text/html').send(getErrorPage(statusCode, isAuthenticated, request));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Return JSON for API clients
|
|
120
|
+
if (!isAuthenticated) {
|
|
110
121
|
return reply.code(401).send({
|
|
111
122
|
error: 'Unauthorized',
|
|
112
123
|
message: authError || 'Authentication required'
|
|
113
124
|
});
|
|
114
125
|
} else {
|
|
115
|
-
// Authenticated but not authorized - return 403
|
|
116
126
|
return reply.code(403).send({
|
|
117
127
|
error: 'Forbidden',
|
|
118
128
|
message: 'Access denied'
|
|
@@ -120,6 +130,222 @@ export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError =
|
|
|
120
130
|
}
|
|
121
131
|
}
|
|
122
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Generate a beautiful error page for browsers
|
|
135
|
+
*/
|
|
136
|
+
function getErrorPage(statusCode, isAuthenticated, request) {
|
|
137
|
+
const is401 = statusCode === 401;
|
|
138
|
+
const title = is401 ? 'Authentication Required' : 'Access Denied';
|
|
139
|
+
const subtitle = is401
|
|
140
|
+
? "This resource is protected. You'll need to sign in to continue."
|
|
141
|
+
: "You're signed in, but you don't have permission to view this resource.";
|
|
142
|
+
|
|
143
|
+
const baseUrl = `${request.protocol}://${request.hostname}`;
|
|
144
|
+
|
|
145
|
+
return `<!DOCTYPE html>
|
|
146
|
+
<html lang="en">
|
|
147
|
+
<head>
|
|
148
|
+
<meta charset="UTF-8">
|
|
149
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
150
|
+
<title>${title} - Solid Server</title>
|
|
151
|
+
<style>
|
|
152
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
153
|
+
|
|
154
|
+
body {
|
|
155
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
156
|
+
min-height: 100vh;
|
|
157
|
+
display: flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
justify-content: center;
|
|
160
|
+
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
|
|
161
|
+
padding: 2rem;
|
|
162
|
+
color: #374151;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.container {
|
|
166
|
+
max-width: 540px;
|
|
167
|
+
width: 100%;
|
|
168
|
+
text-align: center;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.card {
|
|
172
|
+
background: white;
|
|
173
|
+
border-radius: 16px;
|
|
174
|
+
padding: 3rem 2.5rem;
|
|
175
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.icon {
|
|
179
|
+
width: 80px;
|
|
180
|
+
height: 80px;
|
|
181
|
+
margin: 0 auto 1.5rem;
|
|
182
|
+
background: ${is401 ? '#fef3c7' : '#fee2e2'};
|
|
183
|
+
border-radius: 50%;
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
justify-content: center;
|
|
187
|
+
font-size: 2.5rem;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
h1 {
|
|
191
|
+
font-size: 1.75rem;
|
|
192
|
+
font-weight: 600;
|
|
193
|
+
color: #111827;
|
|
194
|
+
margin-bottom: 0.75rem;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.subtitle {
|
|
198
|
+
color: #6b7280;
|
|
199
|
+
font-size: 1.05rem;
|
|
200
|
+
line-height: 1.6;
|
|
201
|
+
margin-bottom: 2rem;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.actions {
|
|
205
|
+
display: flex;
|
|
206
|
+
flex-direction: column;
|
|
207
|
+
gap: 0.75rem;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.btn {
|
|
211
|
+
display: inline-flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
justify-content: center;
|
|
214
|
+
gap: 0.5rem;
|
|
215
|
+
padding: 0.875rem 1.5rem;
|
|
216
|
+
border-radius: 10px;
|
|
217
|
+
font-size: 1rem;
|
|
218
|
+
font-weight: 500;
|
|
219
|
+
text-decoration: none;
|
|
220
|
+
transition: all 0.2s ease;
|
|
221
|
+
cursor: pointer;
|
|
222
|
+
border: none;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.btn-primary {
|
|
226
|
+
background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%);
|
|
227
|
+
color: white;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.btn-primary:hover {
|
|
231
|
+
transform: translateY(-1px);
|
|
232
|
+
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.4);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.btn-secondary {
|
|
236
|
+
background: #f3f4f6;
|
|
237
|
+
color: #374151;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.btn-secondary:hover {
|
|
241
|
+
background: #e5e7eb;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.divider {
|
|
245
|
+
display: flex;
|
|
246
|
+
align-items: center;
|
|
247
|
+
margin: 2rem 0;
|
|
248
|
+
color: #9ca3af;
|
|
249
|
+
font-size: 0.875rem;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.divider::before,
|
|
253
|
+
.divider::after {
|
|
254
|
+
content: '';
|
|
255
|
+
flex: 1;
|
|
256
|
+
height: 1px;
|
|
257
|
+
background: #e5e7eb;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.divider span {
|
|
261
|
+
padding: 0 1rem;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.info-box {
|
|
265
|
+
background: #f0fdf4;
|
|
266
|
+
border: 1px solid #bbf7d0;
|
|
267
|
+
border-radius: 10px;
|
|
268
|
+
padding: 1.25rem;
|
|
269
|
+
text-align: left;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.info-box h3 {
|
|
273
|
+
font-size: 0.9rem;
|
|
274
|
+
font-weight: 600;
|
|
275
|
+
color: #166534;
|
|
276
|
+
margin-bottom: 0.5rem;
|
|
277
|
+
display: flex;
|
|
278
|
+
align-items: center;
|
|
279
|
+
gap: 0.5rem;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.info-box p {
|
|
283
|
+
font-size: 0.875rem;
|
|
284
|
+
color: #15803d;
|
|
285
|
+
line-height: 1.5;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.footer {
|
|
289
|
+
margin-top: 2rem;
|
|
290
|
+
font-size: 0.8rem;
|
|
291
|
+
color: #9ca3af;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.footer a {
|
|
295
|
+
color: #7c3aed;
|
|
296
|
+
text-decoration: none;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.footer a:hover {
|
|
300
|
+
text-decoration: underline;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.status-code {
|
|
304
|
+
font-size: 0.75rem;
|
|
305
|
+
color: #9ca3af;
|
|
306
|
+
margin-top: 1rem;
|
|
307
|
+
}
|
|
308
|
+
</style>
|
|
309
|
+
</head>
|
|
310
|
+
<body>
|
|
311
|
+
<div class="container">
|
|
312
|
+
<div class="card">
|
|
313
|
+
<div class="icon">${is401 ? '🔐' : '🚫'}</div>
|
|
314
|
+
<h1>${title}</h1>
|
|
315
|
+
<p class="subtitle">${subtitle}</p>
|
|
316
|
+
|
|
317
|
+
<div class="actions">
|
|
318
|
+
${is401 ? `<a href="${baseUrl}/.account/login/password" class="btn btn-primary">
|
|
319
|
+
Sign In
|
|
320
|
+
</a>` : ''}
|
|
321
|
+
<a href="${baseUrl}/" class="btn btn-secondary">
|
|
322
|
+
Go to Homepage
|
|
323
|
+
</a>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<div class="divider"><span>What is this?</span></div>
|
|
327
|
+
|
|
328
|
+
<div class="info-box">
|
|
329
|
+
<h3>🏖️ Welcome to Solid</h3>
|
|
330
|
+
<p>
|
|
331
|
+
This is a <strong>Solid Pod</strong> — a personal data store where you control your own data.
|
|
332
|
+
Resources can be private, shared with specific people, or public.
|
|
333
|
+
${is401 ? 'Sign in with your WebID to access protected content.' : 'Ask the owner to grant you access.'}
|
|
334
|
+
</p>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<p class="status-code">HTTP ${statusCode} • ${request.url}</p>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<p class="footer">
|
|
341
|
+
Powered by <a href="https://sandy-mount.com">Sandymount</a> •
|
|
342
|
+
<a href="https://solidproject.org">Learn about Solid</a>
|
|
343
|
+
</p>
|
|
344
|
+
</div>
|
|
345
|
+
</body>
|
|
346
|
+
</html>`;
|
|
347
|
+
}
|
|
348
|
+
|
|
123
349
|
/**
|
|
124
350
|
* Authorize access to ACL files
|
|
125
351
|
* ACL files require acl:Control permission on the resource they protect
|
package/src/server.js
CHANGED
|
@@ -232,7 +232,7 @@ export function createServer(options = {}) {
|
|
|
232
232
|
// Security: Block access to dotfiles except allowed Solid-specific ones
|
|
233
233
|
// This prevents exposure of .git/, .env, .htpasswd, etc.
|
|
234
234
|
// Git protocol requests bypass this check when git is enabled
|
|
235
|
-
const ALLOWED_DOTFILES = ['.well-known', '.acl', '.meta', '.pods', '.notifications'];
|
|
235
|
+
const ALLOWED_DOTFILES = ['.well-known', '.acl', '.meta', '.pods', '.notifications', '.account'];
|
|
236
236
|
fastify.addHook('onRequest', async (request, reply) => {
|
|
237
237
|
// Allow git protocol requests through when git is enabled
|
|
238
238
|
if (gitEnabled && isGitRequest(request.url)) {
|
|
@@ -316,7 +316,7 @@ export function createServer(options = {}) {
|
|
|
316
316
|
reply.header('WAC-Allow', wacAllow);
|
|
317
317
|
|
|
318
318
|
if (!authorized) {
|
|
319
|
-
return handleUnauthorized(reply, webId !== null, wacAllow, authError);
|
|
319
|
+
return handleUnauthorized(request, reply, webId !== null, wacAllow, authError);
|
|
320
320
|
}
|
|
321
321
|
});
|
|
322
322
|
|