javascript-solid-server 0.0.3 → 0.0.6

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,211 @@
1
+ /**
2
+ * Solid-OIDC tests
3
+ * Tests for DPoP token verification
4
+ */
5
+
6
+ import { describe, it, before, after } from 'node:test';
7
+ import assert from 'node:assert';
8
+ import * as jose from 'jose';
9
+ import {
10
+ startTestServer,
11
+ stopTestServer,
12
+ request,
13
+ createTestPod,
14
+ getBaseUrl,
15
+ assertStatus
16
+ } from './helpers.js';
17
+
18
+ describe('Solid-OIDC', () => {
19
+ let keyPair;
20
+ let publicJwk;
21
+
22
+ before(async () => {
23
+ await startTestServer();
24
+ await createTestPod('oidctest');
25
+
26
+ // Generate a key pair for testing
27
+ keyPair = await jose.generateKeyPair('ES256');
28
+ publicJwk = await jose.exportJWK(keyPair.publicKey);
29
+ publicJwk.alg = 'ES256';
30
+ });
31
+
32
+ after(async () => {
33
+ await stopTestServer();
34
+ });
35
+
36
+ describe('DPoP Header Parsing', () => {
37
+ // Use private folder - requires authentication
38
+ const privatePath = '/oidctest/private/';
39
+
40
+ it('should reject requests with DPoP auth but no DPoP proof', async () => {
41
+ const res = await fetch(`${getBaseUrl()}${privatePath}`, {
42
+ headers: {
43
+ 'Authorization': 'DPoP some-token'
44
+ }
45
+ });
46
+
47
+ assertStatus(res, 401);
48
+ const body = await res.json();
49
+ assert.ok(body.message.includes('DPoP proof'), 'Should mention DPoP proof');
50
+ });
51
+
52
+ it('should reject invalid DPoP proof JWT', async () => {
53
+ const res = await fetch(`${getBaseUrl()}${privatePath}`, {
54
+ headers: {
55
+ 'Authorization': 'DPoP some-token',
56
+ 'DPoP': 'not-a-valid-jwt'
57
+ }
58
+ });
59
+
60
+ assertStatus(res, 401);
61
+ });
62
+
63
+ it('should reject DPoP proof with wrong type', async () => {
64
+ // Create a JWT that's not a DPoP proof (wrong typ)
65
+ const wrongTypeJwt = await new jose.SignJWT({
66
+ htm: 'GET',
67
+ htu: `${getBaseUrl()}${privatePath}`,
68
+ iat: Math.floor(Date.now() / 1000),
69
+ jti: crypto.randomUUID()
70
+ })
71
+ .setProtectedHeader({ alg: 'ES256', typ: 'JWT', jwk: publicJwk })
72
+ .sign(keyPair.privateKey);
73
+
74
+ const res = await fetch(`${getBaseUrl()}${privatePath}`, {
75
+ headers: {
76
+ 'Authorization': 'DPoP some-token',
77
+ 'DPoP': wrongTypeJwt
78
+ }
79
+ });
80
+
81
+ assertStatus(res, 401);
82
+ });
83
+
84
+ it('should reject DPoP proof with wrong HTTP method', async () => {
85
+ const dpopProof = await createDpopProof('POST', `${getBaseUrl()}${privatePath}`);
86
+
87
+ const res = await fetch(`${getBaseUrl()}${privatePath}`, {
88
+ method: 'GET',
89
+ headers: {
90
+ 'Authorization': 'DPoP some-token',
91
+ 'DPoP': dpopProof
92
+ }
93
+ });
94
+
95
+ assertStatus(res, 401);
96
+ });
97
+
98
+ it('should reject DPoP proof with wrong URL', async () => {
99
+ const dpopProof = await createDpopProof('GET', 'https://other-server.example/');
100
+
101
+ const res = await fetch(`${getBaseUrl()}${privatePath}`, {
102
+ headers: {
103
+ 'Authorization': 'DPoP some-token',
104
+ 'DPoP': dpopProof
105
+ }
106
+ });
107
+
108
+ assertStatus(res, 401);
109
+ });
110
+
111
+ it('should reject expired DPoP proof', async () => {
112
+ // Create a DPoP proof with old iat
113
+ const dpopProof = await new jose.SignJWT({
114
+ htm: 'GET',
115
+ htu: `${getBaseUrl()}${privatePath}`,
116
+ iat: Math.floor(Date.now() / 1000) - 600, // 10 minutes ago
117
+ jti: crypto.randomUUID()
118
+ })
119
+ .setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk })
120
+ .sign(keyPair.privateKey);
121
+
122
+ const res = await fetch(`${getBaseUrl()}${privatePath}`, {
123
+ headers: {
124
+ 'Authorization': 'DPoP some-token',
125
+ 'DPoP': dpopProof
126
+ }
127
+ });
128
+
129
+ assertStatus(res, 401);
130
+ });
131
+
132
+ it('should reject DPoP proof missing jti', async () => {
133
+ const dpopProof = await new jose.SignJWT({
134
+ htm: 'GET',
135
+ htu: `${getBaseUrl()}${privatePath}`,
136
+ iat: Math.floor(Date.now() / 1000)
137
+ // missing jti
138
+ })
139
+ .setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk })
140
+ .sign(keyPair.privateKey);
141
+
142
+ const res = await fetch(`${getBaseUrl()}${privatePath}`, {
143
+ headers: {
144
+ 'Authorization': 'DPoP some-token',
145
+ 'DPoP': dpopProof
146
+ }
147
+ });
148
+
149
+ assertStatus(res, 401);
150
+ });
151
+ });
152
+
153
+ describe('Access Token Verification', () => {
154
+ const privatePath = '/oidctest/private/';
155
+
156
+ it('should reject token with invalid issuer (unreachable)', async () => {
157
+ // Create a valid DPoP proof
158
+ const dpopProof = await createDpopProof('GET', `${getBaseUrl()}${privatePath}`);
159
+
160
+ // Create a fake access token with unreachable issuer
161
+ const fakeToken = await new jose.SignJWT({
162
+ webid: 'https://example.com/user#me',
163
+ sub: 'https://example.com/user#me',
164
+ iss: 'https://nonexistent-idp.example.com',
165
+ aud: 'solid',
166
+ iat: Math.floor(Date.now() / 1000),
167
+ exp: Math.floor(Date.now() / 1000) + 3600
168
+ })
169
+ .setProtectedHeader({ alg: 'ES256' })
170
+ .sign(keyPair.privateKey);
171
+
172
+ const res = await fetch(`${getBaseUrl()}${privatePath}`, {
173
+ headers: {
174
+ 'Authorization': `DPoP ${fakeToken}`,
175
+ 'DPoP': dpopProof
176
+ }
177
+ });
178
+
179
+ assertStatus(res, 401);
180
+ });
181
+ });
182
+
183
+ describe('Bearer Token Fallback', () => {
184
+ it('should still accept simple Bearer tokens', async () => {
185
+ // This should work with our simple token system
186
+ const res = await request('/oidctest/public/', { auth: 'oidctest' });
187
+ assertStatus(res, 200);
188
+ });
189
+
190
+ it('should still accept simple Bearer tokens for writes', async () => {
191
+ const res = await request('/oidctest/public/solid-oidc-test.txt', {
192
+ method: 'PUT',
193
+ body: 'test content',
194
+ auth: 'oidctest'
195
+ });
196
+ assertStatus(res, 201);
197
+ });
198
+ });
199
+
200
+ // Helper to create DPoP proofs
201
+ async function createDpopProof(method, uri) {
202
+ return new jose.SignJWT({
203
+ htm: method,
204
+ htu: uri,
205
+ iat: Math.floor(Date.now() / 1000),
206
+ jti: crypto.randomUUID()
207
+ })
208
+ .setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk })
209
+ .sign(keyPair.privateKey);
210
+ }
211
+ });
@@ -0,0 +1,189 @@
1
+ /**
2
+ * WAC (Web Access Control) tests
3
+ */
4
+
5
+ import { describe, it, before, after } from 'node:test';
6
+ import assert from 'node:assert';
7
+ import {
8
+ startTestServer,
9
+ stopTestServer,
10
+ request,
11
+ createTestPod,
12
+ assertStatus,
13
+ assertHeader,
14
+ getBaseUrl
15
+ } from './helpers.js';
16
+ import { parseAcl, AccessMode, generateOwnerAcl, serializeAcl } from '../src/wac/parser.js';
17
+ import { checkAccess, getRequiredMode } from '../src/wac/checker.js';
18
+
19
+ describe('WAC Parser', () => {
20
+ describe('parseAcl', () => {
21
+ it('should parse a simple ACL', () => {
22
+ const acl = {
23
+ '@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' },
24
+ '@graph': [{
25
+ '@id': '#owner',
26
+ '@type': 'acl:Authorization',
27
+ 'acl:agent': { '@id': 'https://alice.example/#me' },
28
+ 'acl:accessTo': { '@id': 'https://alice.example/resource' },
29
+ 'acl:mode': [{ '@id': 'acl:Read' }, { '@id': 'acl:Write' }]
30
+ }]
31
+ };
32
+
33
+ const auths = parseAcl(JSON.stringify(acl), 'https://alice.example/.acl');
34
+
35
+ assert.strictEqual(auths.length, 1);
36
+ assert.ok(auths[0].agents.includes('https://alice.example/#me'));
37
+ assert.ok(auths[0].modes.includes(AccessMode.READ));
38
+ assert.ok(auths[0].modes.includes(AccessMode.WRITE));
39
+ });
40
+
41
+ it('should parse public access', () => {
42
+ const acl = {
43
+ '@context': { 'acl': 'http://www.w3.org/ns/auth/acl#', 'foaf': 'http://xmlns.com/foaf/0.1/' },
44
+ '@graph': [{
45
+ '@id': '#public',
46
+ '@type': 'acl:Authorization',
47
+ 'acl:agentClass': { '@id': 'foaf:Agent' },
48
+ 'acl:accessTo': { '@id': 'https://alice.example/public/' },
49
+ 'acl:mode': [{ '@id': 'acl:Read' }]
50
+ }]
51
+ };
52
+
53
+ const auths = parseAcl(JSON.stringify(acl), 'https://alice.example/public/.acl');
54
+
55
+ assert.strictEqual(auths.length, 1);
56
+ assert.ok(auths[0].agentClasses.includes('foaf:Agent'));
57
+ assert.ok(auths[0].modes.includes(AccessMode.READ));
58
+ });
59
+
60
+ it('should parse default authorizations for containers', () => {
61
+ const acl = {
62
+ '@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' },
63
+ '@graph': [{
64
+ '@id': '#default',
65
+ '@type': 'acl:Authorization',
66
+ 'acl:agent': { '@id': 'https://alice.example/#me' },
67
+ 'acl:default': { '@id': 'https://alice.example/folder/' },
68
+ 'acl:mode': [{ '@id': 'acl:Read' }]
69
+ }]
70
+ };
71
+
72
+ const auths = parseAcl(JSON.stringify(acl), 'https://alice.example/folder/.acl');
73
+
74
+ assert.strictEqual(auths.length, 1);
75
+ assert.ok(auths[0].default.includes('https://alice.example/folder/'));
76
+ });
77
+
78
+ it('should handle invalid JSON gracefully', () => {
79
+ const auths = parseAcl('not valid json', 'https://example.com/.acl');
80
+ assert.strictEqual(auths.length, 0);
81
+ });
82
+ });
83
+
84
+ describe('generateOwnerAcl', () => {
85
+ it('should generate owner ACL with public read', () => {
86
+ const acl = generateOwnerAcl('https://alice.example/', 'https://alice.example/#me', true);
87
+
88
+ assert.ok(acl['@graph'].length >= 2);
89
+
90
+ // Find owner auth
91
+ const ownerAuth = acl['@graph'].find(a => a['@id'] === '#owner');
92
+ assert.ok(ownerAuth);
93
+ assert.strictEqual(ownerAuth['acl:agent']['@id'], 'https://alice.example/#me');
94
+
95
+ // Find public auth
96
+ const publicAuth = acl['@graph'].find(a => a['@id'] === '#public');
97
+ assert.ok(publicAuth);
98
+ });
99
+ });
100
+ });
101
+
102
+ describe('WAC Checker', () => {
103
+ describe('getRequiredMode', () => {
104
+ it('should return READ for GET', () => {
105
+ assert.strictEqual(getRequiredMode('GET'), AccessMode.READ);
106
+ });
107
+
108
+ it('should return READ for HEAD', () => {
109
+ assert.strictEqual(getRequiredMode('HEAD'), AccessMode.READ);
110
+ });
111
+
112
+ it('should return APPEND for POST', () => {
113
+ assert.strictEqual(getRequiredMode('POST'), AccessMode.APPEND);
114
+ });
115
+
116
+ it('should return WRITE for PUT', () => {
117
+ assert.strictEqual(getRequiredMode('PUT'), AccessMode.WRITE);
118
+ });
119
+
120
+ it('should return WRITE for DELETE', () => {
121
+ assert.strictEqual(getRequiredMode('DELETE'), AccessMode.WRITE);
122
+ });
123
+ });
124
+ });
125
+
126
+ describe('WAC Integration', () => {
127
+ let baseUrl;
128
+
129
+ before(async () => {
130
+ const result = await startTestServer();
131
+ baseUrl = result.baseUrl;
132
+ await createTestPod('wactest');
133
+ });
134
+
135
+ after(async () => {
136
+ await stopTestServer();
137
+ });
138
+
139
+ describe('ACL Files', () => {
140
+ it('should create root .acl on pod creation', async () => {
141
+ const res = await request('/wactest/.acl');
142
+
143
+ assertStatus(res, 200);
144
+ const content = await res.json();
145
+ assert.ok(content['@graph'], 'Should be JSON-LD');
146
+ });
147
+
148
+ it('should create private folder .acl', async () => {
149
+ const res = await request('/wactest/private/.acl');
150
+
151
+ assertStatus(res, 200);
152
+ const content = await res.json();
153
+ assert.ok(content['@graph']);
154
+
155
+ // Should only have owner, no public
156
+ const hasPublic = content['@graph'].some(a =>
157
+ a['acl:agentClass'] && a['acl:agentClass']['@id'] === 'foaf:Agent'
158
+ );
159
+ assert.ok(!hasPublic, 'Private folder should not have public access');
160
+ });
161
+
162
+ it('should create inbox .acl with public append', async () => {
163
+ const res = await request('/wactest/inbox/.acl');
164
+
165
+ assertStatus(res, 200);
166
+ const content = await res.json();
167
+
168
+ // Should have public append
169
+ const publicAuth = content['@graph'].find(a =>
170
+ a['acl:agentClass'] && a['acl:agentClass']['@id'] === 'foaf:Agent'
171
+ );
172
+ assert.ok(publicAuth, 'Inbox should have public access');
173
+
174
+ const modes = publicAuth['acl:mode'].map(m => m['@id']);
175
+ assert.ok(modes.includes('acl:Append'), 'Public should have Append');
176
+ assert.ok(!modes.includes('acl:Read'), 'Public should not have Read');
177
+ });
178
+ });
179
+
180
+ describe('WAC-Allow Header', () => {
181
+ it('should return WAC-Allow header for public container', async () => {
182
+ const res = await request('/wactest/public/');
183
+
184
+ assertHeader(res, 'WAC-Allow');
185
+ const wacAllow = res.headers.get('WAC-Allow');
186
+ assert.ok(wacAllow.includes('public='), 'Should have public permissions');
187
+ });
188
+ });
189
+ });
@@ -1,44 +0,0 @@
1
- {
2
- "timestamp": "2025-03-31T14:25:24.234Z",
3
- "server": "http://nostr.social:3000",
4
- "testDuration": 30000,
5
- "averageResponseTimes": {
6
- "register": 49.28774029878249,
7
- "login": 49.07647125548627,
8
- "read": 145.50252931779679,
9
- "write": 143.20483738812226,
10
- "delete": 145.5004399698014
11
- },
12
- "throughputResults": [
13
- {
14
- "concurrentUsers": 1,
15
- "operations": 300,
16
- "duration": 629.3636230230331,
17
- "throughput": 476.67197312581374
18
- },
19
- {
20
- "concurrentUsers": 5,
21
- "operations": 1500,
22
- "duration": 3072.7389999628067,
23
- "throughput": 488.16381736885444
24
- },
25
- {
26
- "concurrentUsers": 10,
27
- "operations": 3000,
28
- "duration": 6042.516402959824,
29
- "throughput": 496.4818959416479
30
- },
31
- {
32
- "concurrentUsers": 50,
33
- "operations": 14000,
34
- "duration": 30000,
35
- "throughput": 466.6666666666667
36
- },
37
- {
38
- "concurrentUsers": 100,
39
- "operations": 13900,
40
- "duration": 30000,
41
- "throughput": 463.3333333333333
42
- }
43
- ]
44
- }