javascript-solid-server 0.0.20 → 0.0.21

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.
@@ -65,7 +65,9 @@
65
65
  "Bash(pm2 start:*)",
66
66
  "Bash(DATA_ROOT=/home/melvin/jss/data pm2 start:*)",
67
67
  "Bash(pm2 save:*)",
68
- "Bash(gh issue create:*)"
68
+ "Bash(gh issue create:*)",
69
+ "Bash(gh issue view:*)",
70
+ "Bash(gh issue edit:*)"
69
71
  ]
70
72
  }
71
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -121,6 +121,63 @@ export async function handlePost(request, reply) {
121
121
  return reply.code(201).send();
122
122
  }
123
123
 
124
+ /**
125
+ * Create pod directory structure (reusable for registration)
126
+ * @param {string} name - Pod name (username)
127
+ * @param {string} webId - User's WebID URI
128
+ * @param {string} baseUrl - Base URL (without trailing slash)
129
+ */
130
+ export async function createPodStructure(name, webId, baseUrl) {
131
+ const podPath = `/${name}/`;
132
+ const podUri = `${baseUrl}/${name}/`;
133
+ const issuer = baseUrl + '/';
134
+
135
+ // Create pod directory structure
136
+ await storage.createContainer(podPath);
137
+ await storage.createContainer(`${podPath}inbox/`);
138
+ await storage.createContainer(`${podPath}public/`);
139
+ await storage.createContainer(`${podPath}private/`);
140
+ await storage.createContainer(`${podPath}settings/`);
141
+
142
+ // Generate and write WebID profile as index.html at pod root
143
+ const profileHtml = generateProfile({ webId, name, podUri, issuer });
144
+ await storage.write(`${podPath}index.html`, profileHtml);
145
+
146
+ // Generate and write preferences
147
+ const prefs = generatePreferences({ webId, podUri });
148
+ await storage.write(`${podPath}settings/prefs`, serialize(prefs));
149
+
150
+ // Generate and write type indexes
151
+ const publicTypeIndex = generateTypeIndex(`${podUri}settings/publicTypeIndex`);
152
+ await storage.write(`${podPath}settings/publicTypeIndex`, serialize(publicTypeIndex));
153
+
154
+ const privateTypeIndex = generateTypeIndex(`${podUri}settings/privateTypeIndex`);
155
+ await storage.write(`${podPath}settings/privateTypeIndex`, serialize(privateTypeIndex));
156
+
157
+ // Create default ACL files
158
+ // Pod root: owner full control, public read
159
+ const rootAcl = generateOwnerAcl(podUri, webId, true);
160
+ await storage.write(`${podPath}.acl`, serializeAcl(rootAcl));
161
+
162
+ // Private folder: owner only (no public)
163
+ const privateAcl = generatePrivateAcl(`${podUri}private/`, webId);
164
+ await storage.write(`${podPath}private/.acl`, serializeAcl(privateAcl));
165
+
166
+ // Settings folder: owner only
167
+ const settingsAcl = generatePrivateAcl(`${podUri}settings/`, webId);
168
+ await storage.write(`${podPath}settings/.acl`, serializeAcl(settingsAcl));
169
+
170
+ // Inbox: owner full, public append
171
+ const inboxAcl = generateInboxAcl(`${podUri}inbox/`, webId);
172
+ await storage.write(`${podPath}inbox/.acl`, serializeAcl(inboxAcl));
173
+
174
+ // Public folder: owner full, public read (with inheritance)
175
+ const publicAcl = generatePublicFolderAcl(`${podUri}public/`, webId);
176
+ await storage.write(`${podPath}public/.acl`, serializeAcl(publicAcl));
177
+
178
+ return { podPath, podUri };
179
+ }
180
+
124
181
  /**
125
182
  * Create a pod (container) for a user
126
183
  * POST /.pods with { "name": "alice" }
@@ -149,8 +206,8 @@ export async function handleCreatePod(request, reply) {
149
206
  if (!email || typeof email !== 'string') {
150
207
  return reply.code(400).send({ error: 'Email required for account creation' });
151
208
  }
152
- if (!password || password.length < 8) {
153
- return reply.code(400).send({ error: 'Password required (minimum 8 characters)' });
209
+ if (!password) {
210
+ return reply.code(400).send({ error: 'Password required' });
154
211
  }
155
212
  }
156
213
 
@@ -189,49 +246,8 @@ export async function handleCreatePod(request, reply) {
189
246
  const issuer = baseUri + '/';
190
247
 
191
248
  try {
192
- // Create pod directory structure
193
- await storage.createContainer(podPath);
194
- await storage.createContainer(`${podPath}inbox/`);
195
- await storage.createContainer(`${podPath}public/`);
196
- await storage.createContainer(`${podPath}private/`);
197
- await storage.createContainer(`${podPath}settings/`);
198
-
199
- // Generate and write WebID profile as index.html at pod root
200
- const profileHtml = generateProfile({ webId, name, podUri, issuer });
201
- await storage.write(`${podPath}index.html`, profileHtml);
202
-
203
- // Generate and write preferences
204
- const prefs = generatePreferences({ webId, podUri });
205
- await storage.write(`${podPath}settings/prefs`, serialize(prefs));
206
-
207
- // Generate and write type indexes
208
- const publicTypeIndex = generateTypeIndex(`${podUri}settings/publicTypeIndex`);
209
- await storage.write(`${podPath}settings/publicTypeIndex`, serialize(publicTypeIndex));
210
-
211
- const privateTypeIndex = generateTypeIndex(`${podUri}settings/privateTypeIndex`);
212
- await storage.write(`${podPath}settings/privateTypeIndex`, serialize(privateTypeIndex));
213
-
214
- // Create default ACL files
215
- // Pod root: owner full control, public read
216
- const rootAcl = generateOwnerAcl(podUri, webId, true);
217
- await storage.write(`${podPath}.acl`, serializeAcl(rootAcl));
218
-
219
- // Private folder: owner only (no public)
220
- const privateAcl = generatePrivateAcl(`${podUri}private/`, webId);
221
- await storage.write(`${podPath}private/.acl`, serializeAcl(privateAcl));
222
-
223
- // Settings folder: owner only
224
- const settingsAcl = generatePrivateAcl(`${podUri}settings/`, webId);
225
- await storage.write(`${podPath}settings/.acl`, serializeAcl(settingsAcl));
226
-
227
- // Inbox: owner full, public append
228
- const inboxAcl = generateInboxAcl(`${podUri}inbox/`, webId);
229
- await storage.write(`${podPath}inbox/.acl`, serializeAcl(inboxAcl));
230
-
231
- // Public folder: owner full, public read (with inheritance)
232
- const publicAcl = generatePublicFolderAcl(`${podUri}public/`, webId);
233
- await storage.write(`${podPath}public/.acl`, serializeAcl(publicAcl));
234
-
249
+ // Use shared pod creation function
250
+ await createPodStructure(name, webId, baseUri);
235
251
  } catch (err) {
236
252
  console.error('Pod creation error:', err);
237
253
  // Cleanup on failure
@@ -249,7 +265,7 @@ export async function handleCreatePod(request, reply) {
249
265
  if (idpEnabled) {
250
266
  try {
251
267
  const { createAccount } = await import('../idp/accounts.js');
252
- await createAccount({ email, password, webId, podName: name });
268
+ await createAccount({ username: name, email, password, webId, podName: name });
253
269
 
254
270
  return reply.code(201).send({
255
271
  name,
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Account management for the Identity Provider
3
- * Handles user accounts with email/password authentication
3
+ * Handles user accounts with username/password authentication
4
+ * Email is optional - internally uses username@jss if not provided
4
5
  */
5
6
 
6
7
  import bcrypt from 'bcrypt';
@@ -8,6 +9,9 @@ import crypto from 'crypto';
8
9
  import fs from 'fs-extra';
9
10
  import path from 'path';
10
11
 
12
+ // Internal domain for generated emails
13
+ const INTERNAL_DOMAIN = 'jss';
14
+
11
15
  /**
12
16
  * Get accounts directory (computed dynamically to support changing DATA_ROOT)
13
17
  */
@@ -16,6 +20,10 @@ function getAccountsDir() {
16
20
  return path.join(dataRoot, '.idp', 'accounts');
17
21
  }
18
22
 
23
+ function getUsernameIndexPath() {
24
+ return path.join(getAccountsDir(), '_username_index.json');
25
+ }
26
+
19
27
  function getEmailIndexPath() {
20
28
  return path.join(getAccountsDir(), '_email_index.json');
21
29
  }
@@ -55,21 +63,34 @@ async function saveIndex(indexPath, index) {
55
63
  /**
56
64
  * Create a new user account
57
65
  * @param {object} options - Account options
58
- * @param {string} options.email - User email
66
+ * @param {string} options.username - Username (typically same as podName)
59
67
  * @param {string} options.password - Plain text password
60
68
  * @param {string} options.webId - User's WebID URI
61
69
  * @param {string} options.podName - Pod name
70
+ * @param {string} [options.email] - Optional email (defaults to username@jss)
62
71
  * @returns {Promise<object>} - Created account (without password)
63
72
  */
64
- export async function createAccount({ email, password, webId, podName }) {
73
+ export async function createAccount({ username, password, webId, podName, email }) {
65
74
  await ensureDir();
66
75
 
67
- const normalizedEmail = email.toLowerCase().trim();
76
+ const normalizedUsername = username.toLowerCase().trim();
77
+ // Use provided email or generate internal one
78
+ const normalizedEmail = email
79
+ ? email.toLowerCase().trim()
80
+ : `${normalizedUsername}@${INTERNAL_DOMAIN}`;
81
+
82
+ // Check username uniqueness
83
+ const existingByUsername = await findByUsername(normalizedUsername);
84
+ if (existingByUsername) {
85
+ throw new Error('Username already taken');
86
+ }
68
87
 
69
- // Check email uniqueness
70
- const existingByEmail = await findByEmail(normalizedEmail);
71
- if (existingByEmail) {
72
- throw new Error('Email already registered');
88
+ // Check email uniqueness (if real email provided)
89
+ if (email) {
90
+ const existingByEmail = await findByEmail(normalizedEmail);
91
+ if (existingByEmail) {
92
+ throw new Error('Email already registered');
93
+ }
73
94
  }
74
95
 
75
96
  // Check webId uniqueness
@@ -84,6 +105,7 @@ export async function createAccount({ email, password, webId, podName }) {
84
105
 
85
106
  const account = {
86
107
  id,
108
+ username: normalizedUsername,
87
109
  email: normalizedEmail,
88
110
  passwordHash,
89
111
  webId,
@@ -96,6 +118,11 @@ export async function createAccount({ email, password, webId, podName }) {
96
118
  const accountPath = path.join(getAccountsDir(), `${id}.json`);
97
119
  await fs.writeJson(accountPath, account, { spaces: 2 });
98
120
 
121
+ // Update username index
122
+ const usernameIndex = await loadIndex(getUsernameIndexPath());
123
+ usernameIndex[normalizedUsername] = id;
124
+ await saveIndex(getUsernameIndexPath(), usernameIndex);
125
+
99
126
  // Update email index
100
127
  const emailIndex = await loadIndex(getEmailIndexPath());
101
128
  emailIndex[normalizedEmail] = id;
@@ -112,13 +139,17 @@ export async function createAccount({ email, password, webId, podName }) {
112
139
  }
113
140
 
114
141
  /**
115
- * Authenticate a user with email and password
116
- * @param {string} email - User email
142
+ * Authenticate a user with username/email and password
143
+ * @param {string} identifier - Username or email
117
144
  * @param {string} password - Plain text password
118
145
  * @returns {Promise<object|null>} - Account if valid, null if invalid
119
146
  */
120
- export async function authenticate(email, password) {
121
- const account = await findByEmail(email);
147
+ export async function authenticate(identifier, password) {
148
+ // Try to find by username first, then by email
149
+ let account = await findByUsername(identifier);
150
+ if (!account) {
151
+ account = await findByEmail(identifier);
152
+ }
122
153
  if (!account) return null;
123
154
 
124
155
  const valid = await bcrypt.compare(password, account.passwordHash);
@@ -149,6 +180,19 @@ export async function findById(id) {
149
180
  }
150
181
  }
151
182
 
183
+ /**
184
+ * Find an account by username
185
+ * @param {string} username - Username
186
+ * @returns {Promise<object|null>} - Account or null
187
+ */
188
+ export async function findByUsername(username) {
189
+ const normalizedUsername = username.toLowerCase().trim();
190
+ const usernameIndex = await loadIndex(getUsernameIndexPath());
191
+ const id = usernameIndex[normalizedUsername];
192
+ if (!id) return null;
193
+ return findById(id);
194
+ }
195
+
152
196
  /**
153
197
  * Find an account by email
154
198
  * @param {string} email - User email
@@ -201,6 +245,12 @@ export async function deleteAccount(id) {
201
245
  if (!account) return;
202
246
 
203
247
  // Remove from indexes
248
+ if (account.username) {
249
+ const usernameIndex = await loadIndex(getUsernameIndexPath());
250
+ delete usernameIndex[account.username];
251
+ await saveIndex(getUsernameIndexPath(), usernameIndex);
252
+ }
253
+
204
254
  const emailIndex = await loadIndex(getEmailIndexPath());
205
255
  delete emailIndex[account.email];
206
256
  await saveIndex(getEmailIndexPath(), emailIndex);
package/src/idp/index.js CHANGED
@@ -11,6 +11,8 @@ import {
11
11
  handleLogin,
12
12
  handleConsent,
13
13
  handleAbort,
14
+ handleRegisterGet,
15
+ handleRegisterPost,
14
16
  } from './interactions.js';
15
17
  import {
16
18
  handleCredentials,
@@ -220,6 +222,15 @@ export async function idpPlugin(fastify, options) {
220
222
  return handleAbort(request, reply, provider);
221
223
  });
222
224
 
225
+ // Registration routes
226
+ fastify.get('/idp/register', async (request, reply) => {
227
+ return handleRegisterGet(request, reply);
228
+ });
229
+
230
+ fastify.post('/idp/register', async (request, reply) => {
231
+ return handleRegisterPost(request, reply, issuer);
232
+ });
233
+
223
234
  fastify.log.info(`IdP initialized with issuer: ${issuer}`);
224
235
  }
225
236
 
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Interaction handlers for login and consent flows
2
+ * Interaction handlers for login, consent, and registration flows
3
3
  * Handles the user-facing parts of the authentication flow
4
4
  */
5
5
 
6
- import { authenticate, findById } from './accounts.js';
7
- import { loginPage, consentPage, errorPage } from './views.js';
6
+ import { authenticate, findById, createAccount } from './accounts.js';
7
+ import { loginPage, consentPage, errorPage, registerPage } from './views.js';
8
+ import * as storage from '../storage/filesystem.js';
9
+ import { createPodStructure } from '../handlers/container.js';
8
10
 
9
11
  /**
10
12
  * Handle GET /idp/interaction/:uid
@@ -81,11 +83,11 @@ export async function handleLogin(request, reply, provider) {
81
83
  }
82
84
  // If it's already an object, use as-is
83
85
 
84
- // Support both 'email' and 'username' fields for CTH compatibility
85
- const email = parsedBody.email || parsedBody.username;
86
+ // Support username, email, or legacy 'email' field for backwards compatibility
87
+ const identifier = parsedBody.username || parsedBody.email;
86
88
  const password = parsedBody.password;
87
89
 
88
- request.log.info({ email, hasPassword: !!password, bodyType: typeof request.body, keys: Object.keys(parsedBody) }, 'Login attempt');
90
+ request.log.info({ identifier, hasPassword: !!password, bodyType: typeof request.body, keys: Object.keys(parsedBody) }, 'Login attempt');
89
91
 
90
92
  try {
91
93
  const interaction = await provider.Interaction.find(uid);
@@ -94,16 +96,16 @@ export async function handleLogin(request, reply, provider) {
94
96
  }
95
97
 
96
98
  // Validate input
97
- if (!email || !password) {
98
- interaction.lastError = 'Email and password are required';
99
+ if (!identifier || !password) {
100
+ interaction.lastError = 'Username and password are required';
99
101
  await interaction.save(interaction.exp - Math.floor(Date.now() / 1000));
100
102
  return reply.redirect(`/idp/interaction/${uid}`);
101
103
  }
102
104
 
103
105
  // Authenticate
104
- const account = await authenticate(email, password);
106
+ const account = await authenticate(identifier, password);
105
107
  if (!account) {
106
- interaction.lastError = 'Invalid email or password';
108
+ interaction.lastError = 'Invalid username or password';
107
109
  await interaction.save(interaction.exp - Math.floor(Date.now() / 1000));
108
110
  return reply.redirect(`/idp/interaction/${uid}`);
109
111
  }
@@ -291,3 +293,100 @@ export async function handleAbort(request, reply, provider) {
291
293
  return reply.code(500).type('text/html').send(errorPage('Error', err.message));
292
294
  }
293
295
  }
296
+
297
+ /**
298
+ * Handle GET /idp/register
299
+ * Shows registration page
300
+ */
301
+ export async function handleRegisterGet(request, reply) {
302
+ const uid = request.query.uid || null;
303
+ return reply.type('text/html').send(registerPage(uid));
304
+ }
305
+
306
+ /**
307
+ * Handle POST /idp/register
308
+ * Creates account and pod
309
+ */
310
+ export async function handleRegisterPost(request, reply, issuer) {
311
+ const uid = request.query.uid || null;
312
+
313
+ // Parse body
314
+ let parsedBody = request.body || {};
315
+ const contentType = request.headers['content-type'] || '';
316
+
317
+ if (Buffer.isBuffer(parsedBody)) {
318
+ const bodyStr = parsedBody.toString();
319
+ if (contentType.includes('application/json')) {
320
+ try {
321
+ parsedBody = JSON.parse(bodyStr);
322
+ } catch (e) {
323
+ parsedBody = {};
324
+ }
325
+ } else {
326
+ const params = new URLSearchParams(bodyStr);
327
+ parsedBody = Object.fromEntries(params.entries());
328
+ }
329
+ } else if (typeof parsedBody === 'string') {
330
+ const params = new URLSearchParams(parsedBody);
331
+ parsedBody = Object.fromEntries(params.entries());
332
+ }
333
+
334
+ const { username, password, confirmPassword } = parsedBody;
335
+
336
+ // Validate input
337
+ if (!username || !password) {
338
+ return reply.type('text/html').send(registerPage(uid, 'Username and password are required'));
339
+ }
340
+
341
+ // Validate username format
342
+ const usernameRegex = /^[a-z0-9]+$/;
343
+ if (!usernameRegex.test(username)) {
344
+ return reply.type('text/html').send(registerPage(uid, 'Username must contain only lowercase letters and numbers'));
345
+ }
346
+
347
+ if (username.length < 3) {
348
+ return reply.type('text/html').send(registerPage(uid, 'Username must be at least 3 characters'));
349
+ }
350
+
351
+
352
+ if (password !== confirmPassword) {
353
+ return reply.type('text/html').send(registerPage(uid, 'Passwords do not match'));
354
+ }
355
+
356
+ try {
357
+ // Build URLs
358
+ const baseUrl = issuer.endsWith('/') ? issuer.slice(0, -1) : issuer;
359
+ const podUri = `${baseUrl}/${username}/`;
360
+ const webId = `${podUri}#me`;
361
+
362
+ // Check if pod already exists
363
+ const podPath = `${username}/`;
364
+ const podExists = await storage.exists(podPath);
365
+ if (podExists) {
366
+ return reply.type('text/html').send(registerPage(uid, 'Username is already taken'));
367
+ }
368
+
369
+ // Create pod structure
370
+ await createPodStructure(username, webId, baseUrl);
371
+
372
+ // Create account
373
+ await createAccount({
374
+ username,
375
+ password,
376
+ webId,
377
+ podName: username,
378
+ });
379
+
380
+ request.log.info({ username, webId }, 'Account and pod created');
381
+
382
+ // Redirect to login
383
+ if (uid) {
384
+ return reply.redirect(`/idp/interaction/${uid}`);
385
+ } else {
386
+ return reply.type('text/html').send(registerPage(null, null, `Account created! You can now sign in as "${username}".`));
387
+ }
388
+ } catch (err) {
389
+ request.log.error(err, 'Registration error');
390
+ return reply.type('text/html').send(registerPage(uid, err.message));
391
+ }
392
+ }
package/src/idp/views.js CHANGED
@@ -52,6 +52,7 @@ const styles = `
52
52
  color: #333;
53
53
  margin-bottom: 6px;
54
54
  }
55
+ input[type="text"],
55
56
  input[type="email"],
56
57
  input[type="password"] {
57
58
  width: 100%;
@@ -160,6 +161,8 @@ const scopeDescriptions = {
160
161
  * Login page HTML
161
162
  */
162
163
  export function loginPage(uid, clientId, error = null) {
164
+ const appName = clientId || 'An application';
165
+
163
166
  return `
164
167
  <!DOCTYPE html>
165
168
  <html lang="en">
@@ -175,11 +178,16 @@ export function loginPage(uid, clientId, error = null) {
175
178
  <h1>Sign In</h1>
176
179
  <p class="subtitle">Sign in to your Solid Pod</p>
177
180
 
181
+ <div class="client-info">
182
+ <div class="client-name">${escapeHtml(appName)}</div>
183
+ <div class="client-uri">is requesting access to your pod</div>
184
+ </div>
185
+
178
186
  ${error ? `<div class="error">${escapeHtml(error)}</div>` : ''}
179
187
 
180
188
  <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">
189
+ <label for="username">Username</label>
190
+ <input type="text" id="username" name="username" required autofocus placeholder="Your username">
183
191
 
184
192
  <label for="password">Password</label>
185
193
  <input type="password" id="password" name="password" required placeholder="Your password">
@@ -190,6 +198,10 @@ export function loginPage(uid, clientId, error = null) {
190
198
  <form method="POST" action="/idp/interaction/${uid}/abort">
191
199
  <button type="submit" class="btn btn-secondary">Cancel</button>
192
200
  </form>
201
+
202
+ <p style="text-align: center; margin-top: 24px; color: #666; font-size: 14px;">
203
+ Don't have an account? <a href="/idp/register?uid=${uid}" style="color: #0066cc;">Register</a>
204
+ </p>
193
205
  </div>
194
206
  </body>
195
207
  </html>
@@ -281,6 +293,54 @@ export function errorPage(title, message) {
281
293
  `;
282
294
  }
283
295
 
296
+ /**
297
+ * Registration page HTML
298
+ */
299
+ export function registerPage(uid = null, error = null, success = null) {
300
+ return `
301
+ <!DOCTYPE html>
302
+ <html lang="en">
303
+ <head>
304
+ <meta charset="UTF-8">
305
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
306
+ <title>Register - Solid IdP</title>
307
+ <style>${styles}</style>
308
+ </head>
309
+ <body>
310
+ <div class="container">
311
+ <div class="logo">${solidLogo}</div>
312
+ <h1>Create Account</h1>
313
+ <p class="subtitle">Register for a new Solid Pod</p>
314
+
315
+ ${error ? `<div class="error">${escapeHtml(error)}</div>` : ''}
316
+ ${success ? `<div class="error" style="background: #efe; border-color: #cfc; color: #060;">${escapeHtml(success)}</div>` : ''}
317
+
318
+ <form method="POST" action="/idp/register${uid ? `?uid=${uid}` : ''}">
319
+ <label for="username">Username</label>
320
+ <input type="text" id="username" name="username" required autofocus
321
+ placeholder="Choose a username" pattern="[a-z0-9]+"
322
+ title="Lowercase letters and numbers only">
323
+
324
+ <label for="password">Password</label>
325
+ <input type="password" id="password" name="password" required
326
+ placeholder="Choose a password">
327
+
328
+ <label for="confirmPassword">Confirm Password</label>
329
+ <input type="password" id="confirmPassword" name="confirmPassword" required
330
+ placeholder="Confirm your password">
331
+
332
+ <button type="submit" class="btn btn-primary">Create Account</button>
333
+ </form>
334
+
335
+ <p style="text-align: center; margin-top: 24px; color: #666; font-size: 14px;">
336
+ Already have an account? <a href="${uid ? `/idp/interaction/${uid}` : '/idp/auth'}" style="color: #0066cc;">Sign In</a>
337
+ </p>
338
+ </div>
339
+ </body>
340
+ </html>
341
+ `;
342
+ }
343
+
284
344
  /**
285
345
  * Escape HTML to prevent XSS
286
346
  */
package/test/idp.test.js CHANGED
@@ -96,18 +96,6 @@ describe('Identity Provider', () => {
96
96
  assert.ok(body.error.includes('Password'));
97
97
  });
98
98
 
99
- it('should require minimum password length', async () => {
100
- const res = await fetch(`${BASE_URL}/.pods`, {
101
- method: 'POST',
102
- headers: { 'Content-Type': 'application/json' },
103
- body: JSON.stringify({ name: 'shortpass', email: 'test@example.com', password: 'short' }),
104
- });
105
-
106
- assert.strictEqual(res.status, 400);
107
- const body = await res.json();
108
- assert.ok(body.error.includes('8'));
109
- });
110
-
111
99
  it('should create pod with account', async () => {
112
100
  const uniqueId = Date.now();
113
101
  const res = await fetch(`${BASE_URL}/.pods`, {
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "318e7a79-23d5-4e0b-8fa1-b63cfaee87e1",
3
+ "username": "credtest",
4
+ "email": "credtest@example.com",
5
+ "passwordHash": "$2b$10$ITkxFeVH56JBgjDqYASbfuounFozpoVQpBvtsYxCszx2I0PBEX0hq",
6
+ "webId": "http://localhost:3101/credtest/#me",
7
+ "podName": "credtest",
8
+ "createdAt": "2025-12-27T14:33:50.756Z",
9
+ "lastLogin": "2025-12-27T14:33:51.196Z"
10
+ }
@@ -1,3 +1,3 @@
1
1
  {
2
- "credtest@example.com": "00f5d7c4-1da9-4e68-92c9-2b931f7c5750"
2
+ "credtest@example.com": "318e7a79-23d5-4e0b-8fa1-b63cfaee87e1"
3
3
  }
@@ -0,0 +1,3 @@
1
+ {
2
+ "credtest": "318e7a79-23d5-4e0b-8fa1-b63cfaee87e1"
3
+ }
@@ -1,3 +1,3 @@
1
1
  {
2
- "http://localhost:3101/credtest/#me": "00f5d7c4-1da9-4e68-92c9-2b931f7c5750"
2
+ "http://localhost:3101/credtest/#me": "318e7a79-23d5-4e0b-8fa1-b63cfaee87e1"
3
3
  }
@@ -3,20 +3,20 @@
3
3
  "keys": [
4
4
  {
5
5
  "kty": "EC",
6
- "x": "qUZf3Zu9iULTVgQu_ARo02vLb7MedW9t3jzW6EykbAc",
7
- "y": "f7fkZpYdSy-m_0vhcw1l5wh4nnHmWLtuw9DEfZNqMyI",
6
+ "x": "Nf8dDZkLGjtbhOI4-NdDeJpP7jFZ1yRIsLGbg4wWFIU",
7
+ "y": "RlENuTLrM8M6a1UQorqtB3NIS5VXq_gI9lqJMUKDjo8",
8
8
  "crv": "P-256",
9
- "d": "711_8EPt8sDLyzDB174cNsAGvkIdZLMZKPlH-kFZ270",
10
- "kid": "af8ee1cc-110c-4e6e-bd2a-12fd7690c20c",
9
+ "d": "WZKOZkoJBrwF7JfwLXPzpJY2XXNgab-YfqUSIT2Xpfs",
10
+ "kid": "91ebc94d-1ed9-4ded-b017-70f51f2aff2b",
11
11
  "use": "sig",
12
12
  "alg": "ES256",
13
- "iat": 1766844269
13
+ "iat": 1766846030
14
14
  }
15
15
  ]
16
16
  },
17
17
  "cookieKeys": [
18
- "vgN9MVzjMn5ehD1LoDQVnDO_kkTdmCmQbOFhjxwJMnE",
19
- "x8blYyo4zMs0Vm8sXy1XFDeKPIMD34yPw1_LO1vv5z4"
18
+ "V7_pksFGkYdBgSRG_lC9AWIki50H1qzj9-L_T-Q7OC0",
19
+ "hmJQwz_B5QLiHUkncYUHZC7xOtGLrLvQVyBmJ5r-nIo"
20
20
  ],
21
- "createdAt": "2025-12-27T14:04:29.573Z"
21
+ "createdAt": "2025-12-27T14:33:50.653Z"
22
22
  }
@@ -1,9 +0,0 @@
1
- {
2
- "id": "00f5d7c4-1da9-4e68-92c9-2b931f7c5750",
3
- "email": "credtest@example.com",
4
- "passwordHash": "$2b$10$oH0fl4a/riqSc4oXZ1pmLuSQOP9AlRSSrOMg7OlP5O2m.C39mOTL6",
5
- "webId": "http://localhost:3101/credtest/#me",
6
- "podName": "credtest",
7
- "createdAt": "2025-12-27T14:04:29.630Z",
8
- "lastLogin": "2025-12-27T14:04:29.865Z"
9
- }