javascript-solid-server 0.0.20 → 0.0.22

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.
@@ -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
  */
@@ -64,7 +64,7 @@ async function findApplicableAcl(resourceUrl, resourcePath, isContainer) {
64
64
  const content = await storage.read(resourceAclPath);
65
65
  if (content) {
66
66
  const aclUrl = getAclUrl(resourceUrl, isContainer);
67
- const authorizations = parseAcl(content.toString(), aclUrl);
67
+ const authorizations = await parseAcl(content.toString(), aclUrl);
68
68
  return { authorizations, isDefault: false, targetUrl: resourceUrl };
69
69
  }
70
70
  }
@@ -80,7 +80,7 @@ async function findApplicableAcl(resourceUrl, resourcePath, isContainer) {
80
80
  const content = await storage.read(parentAclPath);
81
81
  if (content) {
82
82
  const parentUrl = resourceUrl.substring(0, resourceUrl.lastIndexOf(currentPath)) + parentPath;
83
- const authorizations = parseAcl(content.toString(), parentAclPath);
83
+ const authorizations = await parseAcl(content.toString(), parentAclPath);
84
84
  return { authorizations, isDefault: true, targetUrl: parentUrl };
85
85
  }
86
86
  }
@@ -93,7 +93,7 @@ async function findApplicableAcl(resourceUrl, resourcePath, isContainer) {
93
93
  const content = await storage.read('/.acl');
94
94
  if (content) {
95
95
  const rootUrl = resourceUrl.substring(0, resourceUrl.indexOf('/', 8) + 1);
96
- const authorizations = parseAcl(content.toString(), '/.acl');
96
+ const authorizations = await parseAcl(content.toString(), '/.acl');
97
97
  return { authorizations, isDefault: true, targetUrl: rootUrl };
98
98
  }
99
99
  }
package/src/wac/parser.js CHANGED
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * WAC (Web Access Control) Parser
3
- * Parses JSON-LD .acl files into authorization rules
3
+ * Parses ACL files (JSON-LD or Turtle) into authorization rules
4
4
  */
5
5
 
6
+ import { turtleToJsonLd } from '../rdf/turtle.js';
7
+
6
8
  const ACL = 'http://www.w3.org/ns/auth/acl#';
7
9
  const FOAF = 'http://xmlns.com/foaf/0.1/';
8
10
 
@@ -21,16 +23,31 @@ export const AgentClass = {
21
23
  };
22
24
 
23
25
  /**
24
- * Parse a JSON-LD ACL document
25
- * @param {string|object} content - JSON-LD content (string or parsed object)
26
+ * Parse an ACL document (JSON-LD or Turtle)
27
+ * @param {string|object} content - ACL content (JSON-LD string/object or Turtle string)
26
28
  * @param {string} aclUrl - URL of the ACL document
27
- * @returns {Array<Authorization>} List of authorization rules
29
+ * @returns {Promise<Array<Authorization>>} List of authorization rules
28
30
  */
29
- export function parseAcl(content, aclUrl) {
31
+ export async function parseAcl(content, aclUrl) {
30
32
  let doc;
31
- try {
32
- doc = typeof content === 'string' ? JSON.parse(content) : content;
33
- } catch {
33
+
34
+ // If already an object, use it directly
35
+ if (typeof content === 'object' && content !== null) {
36
+ doc = content;
37
+ } else if (typeof content === 'string') {
38
+ // Try JSON-LD first
39
+ try {
40
+ doc = JSON.parse(content);
41
+ } catch {
42
+ // Not JSON, try Turtle
43
+ try {
44
+ doc = await turtleToJsonLd(content, aclUrl);
45
+ } catch (turtleError) {
46
+ // Neither JSON-LD nor valid Turtle
47
+ return [];
48
+ }
49
+ }
50
+ } else {
34
51
  return [];
35
52
  }
36
53
 
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`, {
package/test/wac.test.js CHANGED
@@ -18,7 +18,7 @@ import { checkAccess, getRequiredMode } from '../src/wac/checker.js';
18
18
 
19
19
  describe('WAC Parser', () => {
20
20
  describe('parseAcl', () => {
21
- it('should parse a simple ACL', () => {
21
+ it('should parse a simple ACL', async () => {
22
22
  const acl = {
23
23
  '@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' },
24
24
  '@graph': [{
@@ -30,7 +30,7 @@ describe('WAC Parser', () => {
30
30
  }]
31
31
  };
32
32
 
33
- const auths = parseAcl(JSON.stringify(acl), 'https://alice.example/.acl');
33
+ const auths = await parseAcl(JSON.stringify(acl), 'https://alice.example/.acl');
34
34
 
35
35
  assert.strictEqual(auths.length, 1);
36
36
  assert.ok(auths[0].agents.includes('https://alice.example/#me'));
@@ -38,7 +38,7 @@ describe('WAC Parser', () => {
38
38
  assert.ok(auths[0].modes.includes(AccessMode.WRITE));
39
39
  });
40
40
 
41
- it('should parse public access', () => {
41
+ it('should parse public access', async () => {
42
42
  const acl = {
43
43
  '@context': { 'acl': 'http://www.w3.org/ns/auth/acl#', 'foaf': 'http://xmlns.com/foaf/0.1/' },
44
44
  '@graph': [{
@@ -50,14 +50,14 @@ describe('WAC Parser', () => {
50
50
  }]
51
51
  };
52
52
 
53
- const auths = parseAcl(JSON.stringify(acl), 'https://alice.example/public/.acl');
53
+ const auths = await parseAcl(JSON.stringify(acl), 'https://alice.example/public/.acl');
54
54
 
55
55
  assert.strictEqual(auths.length, 1);
56
56
  assert.ok(auths[0].agentClasses.includes('foaf:Agent'));
57
57
  assert.ok(auths[0].modes.includes(AccessMode.READ));
58
58
  });
59
59
 
60
- it('should parse default authorizations for containers', () => {
60
+ it('should parse default authorizations for containers', async () => {
61
61
  const acl = {
62
62
  '@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' },
63
63
  '@graph': [{
@@ -69,16 +69,35 @@ describe('WAC Parser', () => {
69
69
  }]
70
70
  };
71
71
 
72
- const auths = parseAcl(JSON.stringify(acl), 'https://alice.example/folder/.acl');
72
+ const auths = await parseAcl(JSON.stringify(acl), 'https://alice.example/folder/.acl');
73
73
 
74
74
  assert.strictEqual(auths.length, 1);
75
75
  assert.ok(auths[0].default.includes('https://alice.example/folder/'));
76
76
  });
77
77
 
78
- it('should handle invalid JSON gracefully', () => {
79
- const auths = parseAcl('not valid json', 'https://example.com/.acl');
78
+ it('should handle invalid JSON gracefully', async () => {
79
+ const auths = await parseAcl('not valid json', 'https://example.com/.acl');
80
80
  assert.strictEqual(auths.length, 0);
81
81
  });
82
+
83
+ it('should parse Turtle ACL format', async () => {
84
+ const turtleAcl = `
85
+ @prefix acl: <http://www.w3.org/ns/auth/acl#>.
86
+
87
+ <#owner>
88
+ a acl:Authorization;
89
+ acl:agent <did:nostr:abc123>;
90
+ acl:accessTo <https://example.com/resource>;
91
+ acl:mode acl:Read, acl:Write.
92
+ `;
93
+
94
+ const auths = await parseAcl(turtleAcl, 'https://example.com/.acl');
95
+
96
+ assert.strictEqual(auths.length, 1);
97
+ assert.ok(auths[0].agents.includes('did:nostr:abc123'));
98
+ assert.ok(auths[0].modes.includes(AccessMode.READ));
99
+ assert.ok(auths[0].modes.includes(AccessMode.WRITE));
100
+ });
82
101
  });
83
102
 
84
103
  describe('generateOwnerAcl', () => {
@@ -1,3 +1,3 @@
1
1
  {
2
- "credtest@example.com": "00f5d7c4-1da9-4e68-92c9-2b931f7c5750"
2
+ "credtest@example.com": "ba3591b1-4653-4c64-9661-57dc355e5acc"
3
3
  }
@@ -0,0 +1,3 @@
1
+ {
2
+ "credtest": "ba3591b1-4653-4c64-9661-57dc355e5acc"
3
+ }
@@ -1,3 +1,3 @@
1
1
  {
2
- "http://localhost:3101/credtest/#me": "00f5d7c4-1da9-4e68-92c9-2b931f7c5750"
2
+ "http://localhost:3101/credtest/#me": "ba3591b1-4653-4c64-9661-57dc355e5acc"
3
3
  }
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "ba3591b1-4653-4c64-9661-57dc355e5acc",
3
+ "username": "credtest",
4
+ "email": "credtest@example.com",
5
+ "passwordHash": "$2b$10$tFYM8KuMVTFRpVMqZOYR4OKNreNLgCBqzZVTNAhpdBFUmGH1MFNBu",
6
+ "webId": "http://localhost:3101/credtest/#me",
7
+ "podName": "credtest",
8
+ "createdAt": "2025-12-28T14:20:02.176Z",
9
+ "lastLogin": "2025-12-28T14:20:02.579Z"
10
+ }
@@ -3,20 +3,20 @@
3
3
  "keys": [
4
4
  {
5
5
  "kty": "EC",
6
- "x": "qUZf3Zu9iULTVgQu_ARo02vLb7MedW9t3jzW6EykbAc",
7
- "y": "f7fkZpYdSy-m_0vhcw1l5wh4nnHmWLtuw9DEfZNqMyI",
6
+ "x": "Aa7l5-YrS54RU8xPfEphUTRwNBzSm6lxm84aqKjfrSg",
7
+ "y": "tWi_lhjqQhd43KdK5YqDg7ZzRSUZo3L0ytbiBTdPOWs",
8
8
  "crv": "P-256",
9
- "d": "711_8EPt8sDLyzDB174cNsAGvkIdZLMZKPlH-kFZ270",
10
- "kid": "af8ee1cc-110c-4e6e-bd2a-12fd7690c20c",
9
+ "d": "x6NqVSfA241O10u9Qp4m0dQZsTNYw-Hku3r0eu47VZE",
10
+ "kid": "ed46f7df-3010-43da-9032-e0acaee4d3e1",
11
11
  "use": "sig",
12
12
  "alg": "ES256",
13
- "iat": 1766844269
13
+ "iat": 1766931602
14
14
  }
15
15
  ]
16
16
  },
17
17
  "cookieKeys": [
18
- "vgN9MVzjMn5ehD1LoDQVnDO_kkTdmCmQbOFhjxwJMnE",
19
- "x8blYyo4zMs0Vm8sXy1XFDeKPIMD34yPw1_LO1vv5z4"
18
+ "Vb3JNLAlJHCOu5u73eUA_rzlc9aJ0_WCQCu9RWV5WL4",
19
+ "5xCVtYihgadSlvy1QRD_DcU4_9mI_Ggn0DrngzPdiyM"
20
20
  ],
21
- "createdAt": "2025-12-27T14:04:29.573Z"
21
+ "createdAt": "2025-12-28T14:20:02.080Z"
22
22
  }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Test script for did:nostr in ACL files
3
+ *
4
+ * Tests:
5
+ * 1. Create a container with restricted access
6
+ * 2. Set ACL with did:nostr agent
7
+ * 3. Verify Nostr auth grants access
8
+ */
9
+
10
+ import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools';
11
+ import { getToken } from 'nostr-tools/nip98';
12
+
13
+ const BASE_URL = process.env.TEST_URL || 'http://localhost:4000';
14
+
15
+ async function main() {
16
+ console.log('=== did:nostr ACL Authorization Test ===\n');
17
+
18
+ // Generate a keypair for testing
19
+ const sk = generateSecretKey();
20
+ const pk = getPublicKey(sk);
21
+ const didNostr = `did:nostr:${pk}`;
22
+
23
+ console.log('1. Generated keypair');
24
+ console.log(` Pubkey: ${pk.slice(0, 16)}...`);
25
+ console.log(` DID: ${didNostr.slice(0, 24)}...\n`);
26
+
27
+ // Create a unique test container
28
+ const testPath = `/demo/nostr-acl-test-${Date.now()}/`;
29
+ const containerUrl = `${BASE_URL}${testPath}`;
30
+
31
+ console.log(`2. Creating test container: ${testPath}`);
32
+
33
+ // Create container (unauthenticated - should work on public parent)
34
+ const createRes = await fetch(containerUrl, {
35
+ method: 'PUT',
36
+ headers: { 'Content-Type': 'text/turtle' },
37
+ body: ''
38
+ });
39
+
40
+ if (!createRes.ok && createRes.status !== 201) {
41
+ console.log(` Failed to create container: ${createRes.status}`);
42
+ // Try anyway
43
+ } else {
44
+ console.log(` Created: ${createRes.status}\n`);
45
+ }
46
+
47
+ // Create ACL with did:nostr agent (Turtle format)
48
+ const aclUrl = `${containerUrl}.acl`;
49
+ const aclContent = `
50
+ @prefix acl: <http://www.w3.org/ns/auth/acl#>.
51
+
52
+ <#nostrAccess>
53
+ a acl:Authorization;
54
+ acl:agent <${didNostr}>;
55
+ acl:accessTo <${containerUrl}>;
56
+ acl:default <${containerUrl}>;
57
+ acl:mode acl:Read, acl:Write, acl:Control.
58
+ `;
59
+
60
+ console.log('3. Creating ACL with did:nostr agent');
61
+ console.log(` ACL URL: ${aclUrl}`);
62
+ console.log(` Agent: ${didNostr.slice(0, 40)}...`);
63
+
64
+ const aclRes = await fetch(aclUrl, {
65
+ method: 'PUT',
66
+ headers: { 'Content-Type': 'text/turtle' },
67
+ body: aclContent
68
+ });
69
+
70
+ console.log(` ACL created: ${aclRes.status}\n`);
71
+
72
+ // Verify ACL was saved correctly
73
+ console.log('4. Verifying ACL content');
74
+ const aclCheck = await fetch(aclUrl, {
75
+ headers: { 'Accept': 'text/turtle' }
76
+ });
77
+ const savedAcl = await aclCheck.text();
78
+ console.log(` ACL response: ${aclCheck.status}`);
79
+ console.log(` Contains did:nostr: ${savedAcl.includes('did:nostr:')}\n`);
80
+
81
+ // Test 1: Access WITHOUT auth (should be denied)
82
+ console.log('5. Testing access WITHOUT auth (should be 401/403)...');
83
+ const noAuthRes = await fetch(containerUrl);
84
+ console.log(` Status: ${noAuthRes.status} ${noAuthRes.status === 401 || noAuthRes.status === 403 ? '✓' : '✗'}\n`);
85
+
86
+ // Test 2: Access WITH correct Nostr auth
87
+ console.log('6. Testing access WITH correct Nostr auth...');
88
+ const token = await getToken(containerUrl, 'GET', (event) => finalizeEvent(event, sk));
89
+
90
+ const authRes = await fetch(containerUrl, {
91
+ headers: {
92
+ 'Authorization': `Nostr ${token}`,
93
+ 'Accept': 'text/turtle'
94
+ }
95
+ });
96
+
97
+ console.log(` Status: ${authRes.status}`);
98
+
99
+ if (authRes.status === 200) {
100
+ console.log(' ✓ ACCESS GRANTED - did:nostr ACL working!\n');
101
+ } else {
102
+ console.log(' ✗ Access denied');
103
+ const body = await authRes.text();
104
+ console.log(` Body: ${body.slice(0, 200)}\n`);
105
+ }
106
+
107
+ // Test 3: Access with DIFFERENT Nostr key (should be denied)
108
+ console.log('7. Testing with DIFFERENT Nostr key (should be denied)...');
109
+ const wrongSk = generateSecretKey();
110
+ const wrongToken = await getToken(containerUrl, 'GET', (event) => finalizeEvent(event, wrongSk));
111
+
112
+ const wrongAuthRes = await fetch(containerUrl, {
113
+ headers: {
114
+ 'Authorization': `Nostr ${wrongToken}`,
115
+ 'Accept': 'text/turtle'
116
+ }
117
+ });
118
+
119
+ console.log(` Status: ${wrongAuthRes.status} ${wrongAuthRes.status === 403 ? '✓' : '✗'}\n`);
120
+
121
+ // Clean up
122
+ console.log('8. Cleaning up test container...');
123
+ const deleteToken = await getToken(containerUrl, 'DELETE', (event) => finalizeEvent(event, sk));
124
+ const deleteRes = await fetch(containerUrl, {
125
+ method: 'DELETE',
126
+ headers: { 'Authorization': `Nostr ${deleteToken}` }
127
+ });
128
+ console.log(` Delete: ${deleteRes.status}\n`);
129
+
130
+ // Summary
131
+ console.log('=== Test Summary ===');
132
+ console.log(`No auth: ${noAuthRes.status === 401 || noAuthRes.status === 403 ? 'PASS' : 'FAIL'} (${noAuthRes.status})`);
133
+ console.log(`Correct key: ${authRes.status === 200 ? 'PASS' : 'FAIL'} (${authRes.status})`);
134
+ console.log(`Wrong key: ${wrongAuthRes.status === 403 ? 'PASS' : 'FAIL'} (${wrongAuthRes.status})`);
135
+
136
+ const allPassed = (noAuthRes.status === 401 || noAuthRes.status === 403) &&
137
+ authRes.status === 200 &&
138
+ wrongAuthRes.status === 403;
139
+
140
+ console.log(`\nOverall: ${allPassed ? 'ALL TESTS PASSED ✓' : 'SOME TESTS FAILED ✗'}`);
141
+ process.exit(allPassed ? 0 : 1);
142
+ }
143
+
144
+ main().catch(console.error);