javascript-solid-server 0.0.67 → 0.0.68

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.
@@ -217,7 +217,9 @@
217
217
  "Bash(gh repo list:*)",
218
218
  "Bash(gh search:*)",
219
219
  "Bash(__NEW_LINE__ echo \"\")",
220
- "WebFetch(domain:webfinger.net)"
220
+ "WebFetch(domain:webfinger.net)",
221
+ "Bash(npm update:*)",
222
+ "Bash(timeout 8 node:*)"
221
223
  ]
222
224
  }
223
225
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.67",
3
+ "version": "0.0.68",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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
@@ -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