javascript-solid-server 0.0.10 → 0.0.12

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.
@@ -0,0 +1,295 @@
1
+ /**
2
+ * HTML templates for IdP login/consent pages
3
+ * Minimal, functional design
4
+ */
5
+
6
+ const styles = `
7
+ * { box-sizing: border-box; }
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
+ background: #f5f5f5;
11
+ margin: 0;
12
+ padding: 40px 20px;
13
+ min-height: 100vh;
14
+ }
15
+ .container {
16
+ max-width: 400px;
17
+ margin: 0 auto;
18
+ background: white;
19
+ border-radius: 12px;
20
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
21
+ padding: 40px;
22
+ }
23
+ h1 {
24
+ margin: 0 0 8px 0;
25
+ font-size: 24px;
26
+ color: #333;
27
+ }
28
+ .subtitle {
29
+ color: #666;
30
+ margin: 0 0 30px 0;
31
+ font-size: 14px;
32
+ }
33
+ .client-info {
34
+ background: #f8f9fa;
35
+ border-radius: 8px;
36
+ padding: 16px;
37
+ margin-bottom: 24px;
38
+ }
39
+ .client-name {
40
+ font-weight: 600;
41
+ color: #333;
42
+ }
43
+ .client-uri {
44
+ font-size: 12px;
45
+ color: #666;
46
+ word-break: break-all;
47
+ }
48
+ label {
49
+ display: block;
50
+ font-size: 14px;
51
+ font-weight: 500;
52
+ color: #333;
53
+ margin-bottom: 6px;
54
+ }
55
+ input[type="email"],
56
+ input[type="password"] {
57
+ width: 100%;
58
+ padding: 12px;
59
+ border: 1px solid #ddd;
60
+ border-radius: 8px;
61
+ font-size: 16px;
62
+ margin-bottom: 16px;
63
+ transition: border-color 0.2s;
64
+ }
65
+ input:focus {
66
+ outline: none;
67
+ border-color: #0066cc;
68
+ }
69
+ .error {
70
+ background: #fee;
71
+ border: 1px solid #fcc;
72
+ color: #c00;
73
+ padding: 12px;
74
+ border-radius: 8px;
75
+ margin-bottom: 20px;
76
+ font-size: 14px;
77
+ }
78
+ .btn {
79
+ display: inline-block;
80
+ padding: 12px 24px;
81
+ border-radius: 8px;
82
+ font-size: 16px;
83
+ font-weight: 500;
84
+ cursor: pointer;
85
+ border: none;
86
+ text-decoration: none;
87
+ text-align: center;
88
+ transition: background-color 0.2s;
89
+ }
90
+ .btn-primary {
91
+ background: #0066cc;
92
+ color: white;
93
+ width: 100%;
94
+ }
95
+ .btn-primary:hover {
96
+ background: #0052a3;
97
+ }
98
+ .btn-secondary {
99
+ background: #f0f0f0;
100
+ color: #333;
101
+ margin-top: 12px;
102
+ width: 100%;
103
+ }
104
+ .btn-secondary:hover {
105
+ background: #e0e0e0;
106
+ }
107
+ .scopes {
108
+ margin: 20px 0;
109
+ }
110
+ .scope {
111
+ display: flex;
112
+ align-items: center;
113
+ padding: 12px;
114
+ background: #f8f9fa;
115
+ border-radius: 8px;
116
+ margin-bottom: 8px;
117
+ }
118
+ .scope-icon {
119
+ width: 24px;
120
+ height: 24px;
121
+ margin-right: 12px;
122
+ opacity: 0.6;
123
+ }
124
+ .scope-name {
125
+ font-weight: 500;
126
+ }
127
+ .scope-desc {
128
+ font-size: 12px;
129
+ color: #666;
130
+ }
131
+ .actions {
132
+ margin-top: 24px;
133
+ }
134
+ .logo {
135
+ text-align: center;
136
+ margin-bottom: 24px;
137
+ }
138
+ .logo svg {
139
+ width: 48px;
140
+ height: 48px;
141
+ }
142
+ `;
143
+
144
+ const solidLogo = `
145
+ <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
146
+ <circle cx="50" cy="50" r="45" fill="#7C4DFF" />
147
+ <path d="M30 50 L45 65 L70 40" stroke="white" stroke-width="8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
148
+ </svg>
149
+ `;
150
+
151
+ const scopeDescriptions = {
152
+ openid: 'Access your identity',
153
+ webid: 'Access your WebID',
154
+ profile: 'Access your name',
155
+ email: 'Access your email address',
156
+ offline_access: 'Stay logged in',
157
+ };
158
+
159
+ /**
160
+ * Login page HTML
161
+ */
162
+ export function loginPage(uid, clientId, error = null) {
163
+ return `
164
+ <!DOCTYPE html>
165
+ <html lang="en">
166
+ <head>
167
+ <meta charset="UTF-8">
168
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
169
+ <title>Sign In - Solid IdP</title>
170
+ <style>${styles}</style>
171
+ </head>
172
+ <body>
173
+ <div class="container">
174
+ <div class="logo">${solidLogo}</div>
175
+ <h1>Sign In</h1>
176
+ <p class="subtitle">Sign in to your Solid Pod</p>
177
+
178
+ ${error ? `<div class="error">${escapeHtml(error)}</div>` : ''}
179
+
180
+ <form method="POST" action="/idp/interaction/${uid}/login">
181
+ <label for="email">Email</label>
182
+ <input type="email" id="email" name="email" required autofocus placeholder="you@example.com">
183
+
184
+ <label for="password">Password</label>
185
+ <input type="password" id="password" name="password" required placeholder="Your password">
186
+
187
+ <button type="submit" class="btn btn-primary">Sign In</button>
188
+ </form>
189
+
190
+ <form method="POST" action="/idp/interaction/${uid}/abort">
191
+ <button type="submit" class="btn btn-secondary">Cancel</button>
192
+ </form>
193
+ </div>
194
+ </body>
195
+ </html>
196
+ `;
197
+ }
198
+
199
+ /**
200
+ * Consent page HTML
201
+ */
202
+ export function consentPage(uid, client, params, account) {
203
+ const scopes = (params.scope || 'openid').split(' ').filter(Boolean);
204
+ const clientName = client?.clientName || client?.client_id || 'Unknown App';
205
+ const clientUri = client?.clientUri || client?.redirect_uris?.[0] || '';
206
+
207
+ const scopeItems = scopes.map(scope => `
208
+ <div class="scope">
209
+ <div class="scope-icon">✓</div>
210
+ <div>
211
+ <div class="scope-name">${escapeHtml(scope)}</div>
212
+ <div class="scope-desc">${escapeHtml(scopeDescriptions[scope] || 'Access requested')}</div>
213
+ </div>
214
+ </div>
215
+ `).join('');
216
+
217
+ return `
218
+ <!DOCTYPE html>
219
+ <html lang="en">
220
+ <head>
221
+ <meta charset="UTF-8">
222
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
223
+ <title>Authorize - Solid IdP</title>
224
+ <style>${styles}</style>
225
+ </head>
226
+ <body>
227
+ <div class="container">
228
+ <div class="logo">${solidLogo}</div>
229
+ <h1>Authorize Access</h1>
230
+ <p class="subtitle">Allow this app to access your data?</p>
231
+
232
+ <div class="client-info">
233
+ <div class="client-name">${escapeHtml(clientName)}</div>
234
+ ${clientUri ? `<div class="client-uri">${escapeHtml(clientUri)}</div>` : ''}
235
+ </div>
236
+
237
+ ${account ? `<p>Signed in as <strong>${escapeHtml(account.email)}</strong></p>` : ''}
238
+
239
+ <div class="scopes">
240
+ <label>This app is requesting access to:</label>
241
+ ${scopeItems}
242
+ </div>
243
+
244
+ <div class="actions">
245
+ <form method="POST" action="/idp/interaction/${uid}/confirm">
246
+ <button type="submit" class="btn btn-primary">Allow Access</button>
247
+ </form>
248
+
249
+ <form method="POST" action="/idp/interaction/${uid}/abort">
250
+ <button type="submit" class="btn btn-secondary">Deny</button>
251
+ </form>
252
+ </div>
253
+ </div>
254
+ </body>
255
+ </html>
256
+ `;
257
+ }
258
+
259
+ /**
260
+ * Error page HTML
261
+ */
262
+ export function errorPage(title, message) {
263
+ return `
264
+ <!DOCTYPE html>
265
+ <html lang="en">
266
+ <head>
267
+ <meta charset="UTF-8">
268
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
269
+ <title>Error - Solid IdP</title>
270
+ <style>${styles}</style>
271
+ </head>
272
+ <body>
273
+ <div class="container">
274
+ <div class="logo">${solidLogo}</div>
275
+ <h1 style="color: #c00;">${escapeHtml(title)}</h1>
276
+ <p>${escapeHtml(message)}</p>
277
+ <a href="/" class="btn btn-secondary">Go Home</a>
278
+ </div>
279
+ </body>
280
+ </html>
281
+ `;
282
+ }
283
+
284
+ /**
285
+ * Escape HTML to prevent XSS
286
+ */
287
+ function escapeHtml(text) {
288
+ if (!text) return '';
289
+ return String(text)
290
+ .replace(/&/g, '&amp;')
291
+ .replace(/</g, '&lt;')
292
+ .replace(/>/g, '&gt;')
293
+ .replace(/"/g, '&quot;')
294
+ .replace(/'/g, '&#039;');
295
+ }
package/src/server.js CHANGED
@@ -4,6 +4,7 @@ import { handlePost, handleCreatePod } from './handlers/container.js';
4
4
  import { getCorsHeaders } from './ldp/headers.js';
5
5
  import { authorize, handleUnauthorized } from './auth/middleware.js';
6
6
  import { notificationsPlugin } from './notifications/index.js';
7
+ import { idpPlugin } from './idp/index.js';
7
8
 
8
9
  /**
9
10
  * Create and configure Fastify server
@@ -11,19 +12,42 @@ import { notificationsPlugin } from './notifications/index.js';
11
12
  * @param {boolean} options.logger - Enable logging (default true)
12
13
  * @param {boolean} options.conneg - Enable content negotiation for RDF (default false)
13
14
  * @param {boolean} options.notifications - Enable WebSocket notifications (default false)
15
+ * @param {boolean} options.idp - Enable built-in Identity Provider (default false)
16
+ * @param {string} options.idpIssuer - IdP issuer URL (default: server URL)
17
+ * @param {object} options.ssl - SSL configuration { key, cert } (default null)
18
+ * @param {string} options.root - Data directory path (default from env or ./data)
14
19
  */
15
20
  export function createServer(options = {}) {
16
21
  // Content negotiation is OFF by default - we're a JSON-LD native server
17
22
  const connegEnabled = options.conneg ?? false;
18
23
  // WebSocket notifications are OFF by default
19
24
  const notificationsEnabled = options.notifications ?? false;
25
+ // Identity Provider is OFF by default
26
+ const idpEnabled = options.idp ?? false;
27
+ const idpIssuer = options.idpIssuer;
20
28
 
21
- const fastify = Fastify({
29
+ // Set data root via environment variable if provided
30
+ if (options.root) {
31
+ process.env.DATA_ROOT = options.root;
32
+ }
33
+
34
+ // Fastify options
35
+ const fastifyOptions = {
22
36
  logger: options.logger ?? true,
23
37
  trustProxy: true,
24
38
  // Handle raw body for non-JSON content
25
39
  bodyLimit: 10 * 1024 * 1024 // 10MB
26
- });
40
+ };
41
+
42
+ // Add HTTPS support if SSL config provided
43
+ if (options.ssl && options.ssl.key && options.ssl.cert) {
44
+ fastifyOptions.https = {
45
+ key: options.ssl.key,
46
+ cert: options.ssl.cert,
47
+ };
48
+ }
49
+
50
+ const fastify = Fastify(fastifyOptions);
27
51
 
28
52
  // Add raw body parser for all content types
29
53
  fastify.addContentTypeParser('*', { parseAs: 'buffer' }, (req, body, done) => {
@@ -33,9 +57,11 @@ export function createServer(options = {}) {
33
57
  // Attach server config to requests
34
58
  fastify.decorateRequest('connegEnabled', null);
35
59
  fastify.decorateRequest('notificationsEnabled', null);
60
+ fastify.decorateRequest('idpEnabled', null);
36
61
  fastify.addHook('onRequest', async (request) => {
37
62
  request.connegEnabled = connegEnabled;
38
63
  request.notificationsEnabled = notificationsEnabled;
64
+ request.idpEnabled = idpEnabled;
39
65
  });
40
66
 
41
67
  // Register WebSocket notifications plugin if enabled
@@ -43,6 +69,11 @@ export function createServer(options = {}) {
43
69
  fastify.register(notificationsPlugin);
44
70
  }
45
71
 
72
+ // Register Identity Provider plugin if enabled
73
+ if (idpEnabled) {
74
+ fastify.register(idpPlugin, { issuer: idpIssuer });
75
+ }
76
+
46
77
  // Global CORS preflight
47
78
  fastify.addHook('onRequest', async (request, reply) => {
48
79
  // Add CORS headers to all responses
@@ -54,21 +85,17 @@ export function createServer(options = {}) {
54
85
  const wsProtocol = request.protocol === 'https' ? 'wss' : 'ws';
55
86
  reply.header('Updates-Via', `${wsProtocol}://${request.hostname}/.notifications`);
56
87
  }
57
-
58
- // Handle preflight OPTIONS
59
- if (request.method === 'OPTIONS') {
60
- // Add Allow header for LDP compliance
61
- reply.header('Allow', 'GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS');
62
- reply.code(204).send();
63
- return reply;
64
- }
88
+ // Note: OPTIONS requests are handled by handleOptions to include Accept-* headers
65
89
  });
66
90
 
67
91
  // Authorization hook - check WAC permissions
68
92
  // Skip for pod creation endpoint (needs special handling)
69
93
  fastify.addHook('preHandler', async (request, reply) => {
70
- // Skip auth for pod creation and OPTIONS
71
- if (request.url === '/.pods' || request.method === 'OPTIONS') {
94
+ // Skip auth for pod creation, OPTIONS, IdP routes, and well-known endpoints
95
+ if (request.url === '/.pods' ||
96
+ request.method === 'OPTIONS' ||
97
+ request.url.startsWith('/idp/') ||
98
+ request.url.startsWith('/.well-known/')) {
72
99
  return;
73
100
  }
74
101