javascript-solid-server 0.0.11 → 0.0.13
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.
- package/.claude/settings.local.json +7 -1
- package/README.md +68 -4
- package/bin/jss.js +22 -4
- package/data/alice/.acl +50 -0
- package/data/alice/inbox/.acl +50 -0
- package/data/alice/index.html +80 -0
- package/data/alice/private/.acl +32 -0
- package/data/alice/public/test.json +1 -0
- package/data/alice/settings/.acl +32 -0
- package/data/alice/settings/prefs +17 -0
- package/data/alice/settings/privateTypeIndex +7 -0
- package/data/alice/settings/publicTypeIndex +7 -0
- package/data/bob/.acl +50 -0
- package/data/bob/inbox/.acl +50 -0
- package/data/bob/index.html +80 -0
- package/data/bob/private/.acl +32 -0
- package/data/bob/settings/.acl +32 -0
- package/data/bob/settings/prefs +17 -0
- package/data/bob/settings/privateTypeIndex +7 -0
- package/data/bob/settings/publicTypeIndex +7 -0
- package/package.json +6 -2
- package/scripts/test-cth-compat.js +369 -0
- package/src/config.js +7 -0
- package/src/handlers/container.js +35 -2
- package/src/idp/accounts.js +258 -0
- package/src/idp/adapter.js +204 -0
- package/src/idp/credentials.js +225 -0
- package/src/idp/index.js +135 -0
- package/src/idp/interactions.js +180 -0
- package/src/idp/keys.js +157 -0
- package/src/idp/provider.js +246 -0
- package/src/idp/views.js +295 -0
- package/src/server.js +18 -2
- package/test/idp.test.js +427 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@context": {
|
|
3
|
+
"acl": "http://www.w3.org/ns/auth/acl#",
|
|
4
|
+
"foaf": "http://xmlns.com/foaf/0.1/"
|
|
5
|
+
},
|
|
6
|
+
"@graph": [
|
|
7
|
+
{
|
|
8
|
+
"@id": "#owner",
|
|
9
|
+
"@type": "acl:Authorization",
|
|
10
|
+
"acl:agent": {
|
|
11
|
+
"@id": "http://localhost:3457/bob/#me"
|
|
12
|
+
},
|
|
13
|
+
"acl:accessTo": {
|
|
14
|
+
"@id": "http://localhost:3457/bob/private/"
|
|
15
|
+
},
|
|
16
|
+
"acl:mode": [
|
|
17
|
+
{
|
|
18
|
+
"@id": "acl:Read"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"@id": "acl:Write"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"@id": "acl:Control"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"acl:default": {
|
|
28
|
+
"@id": "http://localhost:3457/bob/private/"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@context": {
|
|
3
|
+
"acl": "http://www.w3.org/ns/auth/acl#",
|
|
4
|
+
"foaf": "http://xmlns.com/foaf/0.1/"
|
|
5
|
+
},
|
|
6
|
+
"@graph": [
|
|
7
|
+
{
|
|
8
|
+
"@id": "#owner",
|
|
9
|
+
"@type": "acl:Authorization",
|
|
10
|
+
"acl:agent": {
|
|
11
|
+
"@id": "http://localhost:3457/bob/#me"
|
|
12
|
+
},
|
|
13
|
+
"acl:accessTo": {
|
|
14
|
+
"@id": "http://localhost:3457/bob/settings/"
|
|
15
|
+
},
|
|
16
|
+
"acl:mode": [
|
|
17
|
+
{
|
|
18
|
+
"@id": "acl:Read"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"@id": "acl:Write"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"@id": "acl:Control"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"acl:default": {
|
|
28
|
+
"@id": "http://localhost:3457/bob/settings/"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@context": {
|
|
3
|
+
"solid": "http://www.w3.org/ns/solid/terms#",
|
|
4
|
+
"pim": "http://www.w3.org/ns/pim/space#",
|
|
5
|
+
"publicTypeIndex": {
|
|
6
|
+
"@id": "solid:publicTypeIndex",
|
|
7
|
+
"@type": "@id"
|
|
8
|
+
},
|
|
9
|
+
"privateTypeIndex": {
|
|
10
|
+
"@id": "solid:privateTypeIndex",
|
|
11
|
+
"@type": "@id"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"@id": "http://localhost:3457/bob/settings/prefs",
|
|
15
|
+
"publicTypeIndex": "http://localhost:3457/bob/settings/publicTypeIndex",
|
|
16
|
+
"privateTypeIndex": "http://localhost:3457/bob/settings/privateTypeIndex"
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "javascript-solid-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "A minimal, fast Solid server",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -19,15 +19,19 @@
|
|
|
19
19
|
"start": "node bin/jss.js start",
|
|
20
20
|
"dev": "node --watch bin/jss.js start",
|
|
21
21
|
"test": "node --test --test-concurrency=1",
|
|
22
|
+
"test:cth": "node scripts/test-cth-compat.js",
|
|
22
23
|
"benchmark": "node benchmark.js"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
26
|
+
"@fastify/middie": "^8.3.3",
|
|
25
27
|
"@fastify/websocket": "^8.3.1",
|
|
28
|
+
"bcrypt": "^6.0.0",
|
|
26
29
|
"commander": "^14.0.2",
|
|
27
30
|
"fastify": "^4.25.2",
|
|
28
31
|
"fs-extra": "^11.2.0",
|
|
29
32
|
"jose": "^6.1.3",
|
|
30
|
-
"n3": "^1.26.0"
|
|
33
|
+
"n3": "^1.26.0",
|
|
34
|
+
"oidc-provider": "^9.6.0"
|
|
31
35
|
},
|
|
32
36
|
"engines": {
|
|
33
37
|
"node": ">=18.0.0"
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CTH Compatibility Test Script
|
|
4
|
+
*
|
|
5
|
+
* Tests that JSS is configured correctly for the Solid Conformance Test Harness.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node scripts/test-cth-compat.js [--port 3000] [--run-cth]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createServer } from '../src/server.js';
|
|
12
|
+
import fs from 'fs-extra';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
// Configuration
|
|
19
|
+
const PORT = parseInt(process.argv.find(a => a.startsWith('--port='))?.split('=')[1] || '3456');
|
|
20
|
+
const RUN_CTH = process.argv.includes('--run-cth');
|
|
21
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
22
|
+
const DATA_DIR = path.join(__dirname, '../.test-cth-data');
|
|
23
|
+
|
|
24
|
+
// Test users
|
|
25
|
+
const ALICE = {
|
|
26
|
+
name: 'alice',
|
|
27
|
+
email: 'alice@test.local',
|
|
28
|
+
password: 'alicepassword123',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const BOB = {
|
|
32
|
+
name: 'bob',
|
|
33
|
+
email: 'bob@test.local',
|
|
34
|
+
password: 'bobpassword123',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Colors for output
|
|
38
|
+
const GREEN = '\x1b[32m';
|
|
39
|
+
const RED = '\x1b[31m';
|
|
40
|
+
const YELLOW = '\x1b[33m';
|
|
41
|
+
const CYAN = '\x1b[36m';
|
|
42
|
+
const RESET = '\x1b[0m';
|
|
43
|
+
|
|
44
|
+
function log(msg, color = RESET) {
|
|
45
|
+
console.log(`${color}${msg}${RESET}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pass(msg) {
|
|
49
|
+
log(` ✓ ${msg}`, GREEN);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fail(msg) {
|
|
53
|
+
log(` ✗ ${msg}`, RED);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function info(msg) {
|
|
57
|
+
log(` ℹ ${msg}`, CYAN);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
log('\n=== JSS CTH Compatibility Test ===\n', CYAN);
|
|
62
|
+
|
|
63
|
+
let server;
|
|
64
|
+
let passed = 0;
|
|
65
|
+
let failed = 0;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Clean up and prepare
|
|
69
|
+
await fs.remove(DATA_DIR);
|
|
70
|
+
await fs.ensureDir(DATA_DIR);
|
|
71
|
+
|
|
72
|
+
// Start server with IdP
|
|
73
|
+
log('Starting server with IdP enabled...', YELLOW);
|
|
74
|
+
server = createServer({
|
|
75
|
+
logger: false,
|
|
76
|
+
root: DATA_DIR,
|
|
77
|
+
idp: true,
|
|
78
|
+
idpIssuer: BASE_URL,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await server.listen({ port: PORT, host: 'localhost' });
|
|
82
|
+
pass(`Server running at ${BASE_URL}`);
|
|
83
|
+
passed++;
|
|
84
|
+
|
|
85
|
+
// Test 1: OIDC Discovery
|
|
86
|
+
log('\n1. OIDC Discovery', YELLOW);
|
|
87
|
+
{
|
|
88
|
+
const res = await fetch(`${BASE_URL}/.well-known/openid-configuration`);
|
|
89
|
+
if (res.status === 200) {
|
|
90
|
+
const config = await res.json();
|
|
91
|
+
if (config.issuer === BASE_URL) {
|
|
92
|
+
pass('/.well-known/openid-configuration returns valid config');
|
|
93
|
+
passed++;
|
|
94
|
+
} else {
|
|
95
|
+
fail(`Issuer mismatch: expected ${BASE_URL}, got ${config.issuer}`);
|
|
96
|
+
failed++;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
fail(`OIDC discovery returned ${res.status}`);
|
|
100
|
+
failed++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Test 2: JWKS
|
|
105
|
+
log('\n2. JWKS Endpoint', YELLOW);
|
|
106
|
+
{
|
|
107
|
+
const res = await fetch(`${BASE_URL}/.well-known/jwks.json`);
|
|
108
|
+
if (res.status === 200) {
|
|
109
|
+
const jwks = await res.json();
|
|
110
|
+
if (jwks.keys?.length > 0) {
|
|
111
|
+
pass('/.well-known/jwks.json returns keys');
|
|
112
|
+
passed++;
|
|
113
|
+
} else {
|
|
114
|
+
fail('JWKS has no keys');
|
|
115
|
+
failed++;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
fail(`JWKS returned ${res.status}`);
|
|
119
|
+
failed++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Test 3: Create Alice
|
|
124
|
+
log('\n3. Create Test User: Alice', YELLOW);
|
|
125
|
+
let aliceWebId, aliceToken;
|
|
126
|
+
{
|
|
127
|
+
const res = await fetch(`${BASE_URL}/.pods`, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
body: JSON.stringify(ALICE),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (res.status === 201) {
|
|
134
|
+
const data = await res.json();
|
|
135
|
+
aliceWebId = data.webId;
|
|
136
|
+
pass(`Created Alice: ${aliceWebId}`);
|
|
137
|
+
passed++;
|
|
138
|
+
} else {
|
|
139
|
+
const err = await res.text();
|
|
140
|
+
fail(`Failed to create Alice: ${res.status} - ${err}`);
|
|
141
|
+
failed++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Test 4: Create Bob
|
|
146
|
+
log('\n4. Create Test User: Bob', YELLOW);
|
|
147
|
+
let bobWebId;
|
|
148
|
+
{
|
|
149
|
+
const res = await fetch(`${BASE_URL}/.pods`, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: { 'Content-Type': 'application/json' },
|
|
152
|
+
body: JSON.stringify(BOB),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (res.status === 201) {
|
|
156
|
+
const data = await res.json();
|
|
157
|
+
bobWebId = data.webId;
|
|
158
|
+
pass(`Created Bob: ${bobWebId}`);
|
|
159
|
+
passed++;
|
|
160
|
+
} else {
|
|
161
|
+
const err = await res.text();
|
|
162
|
+
fail(`Failed to create Bob: ${res.status} - ${err}`);
|
|
163
|
+
failed++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Test 5: Credentials endpoint - JSON
|
|
168
|
+
log('\n5. Credentials Endpoint (JSON)', YELLOW);
|
|
169
|
+
{
|
|
170
|
+
const res = await fetch(`${BASE_URL}/idp/credentials`, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
173
|
+
body: JSON.stringify({ email: ALICE.email, password: ALICE.password }),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (res.status === 200) {
|
|
177
|
+
const data = await res.json();
|
|
178
|
+
if (data.access_token && data.webid) {
|
|
179
|
+
aliceToken = data.access_token;
|
|
180
|
+
pass(`Got token for Alice (type: ${data.token_type})`);
|
|
181
|
+
passed++;
|
|
182
|
+
} else {
|
|
183
|
+
fail('Missing access_token or webid in response');
|
|
184
|
+
failed++;
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
const err = await res.text();
|
|
188
|
+
fail(`Credentials endpoint failed: ${res.status} - ${err}`);
|
|
189
|
+
failed++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Test 6: Credentials endpoint - Form encoded
|
|
194
|
+
log('\n6. Credentials Endpoint (Form-encoded)', YELLOW);
|
|
195
|
+
{
|
|
196
|
+
const res = await fetch(`${BASE_URL}/idp/credentials`, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
199
|
+
body: `email=${encodeURIComponent(BOB.email)}&password=${encodeURIComponent(BOB.password)}`,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (res.status === 200) {
|
|
203
|
+
const data = await res.json();
|
|
204
|
+
if (data.access_token) {
|
|
205
|
+
pass('Form-encoded credentials work');
|
|
206
|
+
passed++;
|
|
207
|
+
} else {
|
|
208
|
+
fail('Missing access_token in response');
|
|
209
|
+
failed++;
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
const err = await res.text();
|
|
213
|
+
fail(`Form-encoded credentials failed: ${res.status} - ${err}`);
|
|
214
|
+
failed++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Test 7: Invalid credentials
|
|
219
|
+
log('\n7. Invalid Credentials Handling', YELLOW);
|
|
220
|
+
{
|
|
221
|
+
const res = await fetch(`${BASE_URL}/idp/credentials`, {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: { 'Content-Type': 'application/json' },
|
|
224
|
+
body: JSON.stringify({ email: ALICE.email, password: 'wrongpassword' }),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (res.status === 401) {
|
|
228
|
+
pass('Returns 401 for invalid password');
|
|
229
|
+
passed++;
|
|
230
|
+
} else {
|
|
231
|
+
fail(`Expected 401, got ${res.status}`);
|
|
232
|
+
failed++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Test 8: Token authentication
|
|
237
|
+
log('\n8. Token Authentication', YELLOW);
|
|
238
|
+
{
|
|
239
|
+
if (aliceToken) {
|
|
240
|
+
const res = await fetch(`${BASE_URL}/alice/`, {
|
|
241
|
+
headers: { 'Authorization': `Bearer ${aliceToken}` },
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (res.status === 200) {
|
|
245
|
+
pass('Token grants access to own pod');
|
|
246
|
+
passed++;
|
|
247
|
+
} else {
|
|
248
|
+
fail(`Token auth failed: ${res.status}`);
|
|
249
|
+
failed++;
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
fail('No token available to test');
|
|
253
|
+
failed++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Test 9: Write with token
|
|
258
|
+
log('\n9. Write with Token', YELLOW);
|
|
259
|
+
{
|
|
260
|
+
if (aliceToken) {
|
|
261
|
+
const res = await fetch(`${BASE_URL}/alice/public/test.json`, {
|
|
262
|
+
method: 'PUT',
|
|
263
|
+
headers: {
|
|
264
|
+
'Authorization': `Bearer ${aliceToken}`,
|
|
265
|
+
'Content-Type': 'application/ld+json',
|
|
266
|
+
},
|
|
267
|
+
body: JSON.stringify({ '@id': '#test', 'http://example.org/value': 42 }),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if ([200, 201, 204].includes(res.status)) {
|
|
271
|
+
pass('Can write to pod with token');
|
|
272
|
+
passed++;
|
|
273
|
+
} else {
|
|
274
|
+
fail(`Write failed: ${res.status}`);
|
|
275
|
+
failed++;
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
fail('No token available to test');
|
|
279
|
+
failed++;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Test 10: Public read
|
|
284
|
+
log('\n10. Public Read Access', YELLOW);
|
|
285
|
+
{
|
|
286
|
+
const res = await fetch(`${BASE_URL}/alice/public/test.json`);
|
|
287
|
+
|
|
288
|
+
if (res.status === 200) {
|
|
289
|
+
pass('Public resources are readable');
|
|
290
|
+
passed++;
|
|
291
|
+
} else {
|
|
292
|
+
fail(`Public read failed: ${res.status}`);
|
|
293
|
+
failed++;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Summary
|
|
298
|
+
log('\n=== Summary ===\n', CYAN);
|
|
299
|
+
log(`Passed: ${passed}`, GREEN);
|
|
300
|
+
if (failed > 0) {
|
|
301
|
+
log(`Failed: ${failed}`, RED);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Generate CTH env file
|
|
305
|
+
log('\n=== CTH Configuration ===\n', CYAN);
|
|
306
|
+
|
|
307
|
+
const envContent = `# CTH Environment for JSS
|
|
308
|
+
# Generated by test-cth-compat.js
|
|
309
|
+
|
|
310
|
+
SOLID_IDENTITY_PROVIDER=${BASE_URL}
|
|
311
|
+
RESOURCE_SERVER_ROOT=${BASE_URL}
|
|
312
|
+
TEST_CONTAINER=alice/public/
|
|
313
|
+
|
|
314
|
+
USERS_ALICE_WEBID=${aliceWebId || `${BASE_URL}/alice/#me`}
|
|
315
|
+
USERS_ALICE_USERNAME=${ALICE.email}
|
|
316
|
+
USERS_ALICE_PASSWORD=${ALICE.password}
|
|
317
|
+
|
|
318
|
+
USERS_BOB_WEBID=${bobWebId || `${BASE_URL}/bob/#me`}
|
|
319
|
+
USERS_BOB_USERNAME=${BOB.email}
|
|
320
|
+
USERS_BOB_PASSWORD=${BOB.password}
|
|
321
|
+
|
|
322
|
+
LOGIN_ENDPOINT=${BASE_URL}/idp/credentials
|
|
323
|
+
|
|
324
|
+
# For self-signed certs
|
|
325
|
+
ALLOW_SELF_SIGNED_CERTS=true
|
|
326
|
+
`;
|
|
327
|
+
|
|
328
|
+
const envPath = path.join(__dirname, '../cth.env');
|
|
329
|
+
await fs.writeFile(envPath, envContent);
|
|
330
|
+
info(`CTH env file written to: ${envPath}`);
|
|
331
|
+
|
|
332
|
+
log('\nTo run CTH:', YELLOW);
|
|
333
|
+
log(`
|
|
334
|
+
# Keep JSS running with IdP:
|
|
335
|
+
JSS_PORT=${PORT} jss start --idp
|
|
336
|
+
|
|
337
|
+
# In another terminal, run CTH:
|
|
338
|
+
docker run -i --rm \\
|
|
339
|
+
--network host \\
|
|
340
|
+
-v "$(pwd)"/reports:/reports \\
|
|
341
|
+
--env-file=cth.env \\
|
|
342
|
+
solidproject/conformance-test-harness \\
|
|
343
|
+
--output=/reports \\
|
|
344
|
+
--target=jss
|
|
345
|
+
`);
|
|
346
|
+
|
|
347
|
+
if (RUN_CTH) {
|
|
348
|
+
log('\nRunning CTH (this may take a while)...', YELLOW);
|
|
349
|
+
// Would spawn docker here, but keeping server running is complex
|
|
350
|
+
info('CTH auto-run not implemented yet. Run manually with commands above.');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
} catch (err) {
|
|
354
|
+
fail(`Error: ${err.message}`);
|
|
355
|
+
console.error(err);
|
|
356
|
+
failed++;
|
|
357
|
+
} finally {
|
|
358
|
+
// Cleanup
|
|
359
|
+
if (server) {
|
|
360
|
+
await server.close();
|
|
361
|
+
}
|
|
362
|
+
await fs.remove(DATA_DIR);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Exit with error code if any tests failed
|
|
366
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
main();
|
package/src/config.js
CHANGED
|
@@ -29,6 +29,10 @@ export const defaults = {
|
|
|
29
29
|
conneg: false,
|
|
30
30
|
notifications: false,
|
|
31
31
|
|
|
32
|
+
// Identity Provider
|
|
33
|
+
idp: false,
|
|
34
|
+
idpIssuer: null,
|
|
35
|
+
|
|
32
36
|
// Logging
|
|
33
37
|
logger: true,
|
|
34
38
|
quiet: false,
|
|
@@ -51,6 +55,8 @@ const envMap = {
|
|
|
51
55
|
JSS_NOTIFICATIONS: 'notifications',
|
|
52
56
|
JSS_QUIET: 'quiet',
|
|
53
57
|
JSS_CONFIG_PATH: 'configPath',
|
|
58
|
+
JSS_IDP: 'idp',
|
|
59
|
+
JSS_IDP_ISSUER: 'idpIssuer',
|
|
54
60
|
};
|
|
55
61
|
|
|
56
62
|
/**
|
|
@@ -181,5 +187,6 @@ export function printConfig(config) {
|
|
|
181
187
|
console.log(` Multi-user: ${config.multiuser}`);
|
|
182
188
|
console.log(` Conneg: ${config.conneg}`);
|
|
183
189
|
console.log(` Notifications: ${config.notifications}`);
|
|
190
|
+
console.log(` IdP: ${config.idp ? (config.idpIssuer || 'enabled') : 'disabled'}`);
|
|
184
191
|
console.log('─'.repeat(40));
|
|
185
192
|
}
|
|
@@ -110,6 +110,7 @@ export async function handlePost(request, reply) {
|
|
|
110
110
|
/**
|
|
111
111
|
* Create a pod (container) for a user
|
|
112
112
|
* POST /.pods with { "name": "alice" }
|
|
113
|
+
* With IdP enabled: { "name": "alice", "email": "alice@example.com", "password": "secret" }
|
|
113
114
|
*
|
|
114
115
|
* Creates the following structure:
|
|
115
116
|
* /{name}/
|
|
@@ -122,12 +123,23 @@ export async function handlePost(request, reply) {
|
|
|
122
123
|
* /{name}/settings/privateTypeIndex
|
|
123
124
|
*/
|
|
124
125
|
export async function handleCreatePod(request, reply) {
|
|
125
|
-
const { name } = request.body || {};
|
|
126
|
+
const { name, email, password } = request.body || {};
|
|
127
|
+
const idpEnabled = request.idpEnabled;
|
|
126
128
|
|
|
127
129
|
if (!name || typeof name !== 'string') {
|
|
128
130
|
return reply.code(400).send({ error: 'Pod name required' });
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
// If IdP is enabled, require email and password
|
|
134
|
+
if (idpEnabled) {
|
|
135
|
+
if (!email || typeof email !== 'string') {
|
|
136
|
+
return reply.code(400).send({ error: 'Email required for account creation' });
|
|
137
|
+
}
|
|
138
|
+
if (!password || password.length < 8) {
|
|
139
|
+
return reply.code(400).send({ error: 'Password required (minimum 8 characters)' });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
131
143
|
// Validate pod name (alphanumeric, dash, underscore)
|
|
132
144
|
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
133
145
|
return reply.code(400).send({ error: 'Invalid pod name. Use alphanumeric, dash, or underscore only.' });
|
|
@@ -200,7 +212,28 @@ export async function handleCreatePod(request, reply) {
|
|
|
200
212
|
|
|
201
213
|
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
202
214
|
|
|
203
|
-
//
|
|
215
|
+
// If IdP is enabled, create account instead of simple token
|
|
216
|
+
if (idpEnabled) {
|
|
217
|
+
try {
|
|
218
|
+
const { createAccount } = await import('../idp/accounts.js');
|
|
219
|
+
await createAccount({ email, password, webId, podName: name });
|
|
220
|
+
|
|
221
|
+
return reply.code(201).send({
|
|
222
|
+
name,
|
|
223
|
+
webId,
|
|
224
|
+
podUri,
|
|
225
|
+
idpIssuer: issuer,
|
|
226
|
+
loginUrl: `${issuer}/idp/auth`,
|
|
227
|
+
});
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error('Account creation error:', err);
|
|
230
|
+
// Rollback pod creation on account failure
|
|
231
|
+
await storage.remove(podPath);
|
|
232
|
+
return reply.code(409).send({ error: err.message });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Generate token for the pod owner (simple auth mode)
|
|
204
237
|
const token = createToken(webId);
|
|
205
238
|
|
|
206
239
|
return reply.code(201).send({
|