javascript-solid-server 0.0.144 → 0.0.146
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 +2 -1
- package/package.json +1 -1
- package/src/handlers/container.js +5 -1
- package/src/server.js +4 -0
- package/src/utils/url.js +14 -1
- package/test/conneg.test.js +67 -0
- package/test/idp.test.js +49 -0
- package/test/pod.test.js +16 -0
- package/test/url.test.js +46 -1
|
@@ -350,7 +350,8 @@
|
|
|
350
350
|
"Bash(\\\\\"git config:*)",
|
|
351
351
|
"Read(//usr/local/lib/node_modules/gitmark-test/**)",
|
|
352
352
|
"WebFetch(domain:nip98.com)",
|
|
353
|
-
"WebFetch(domain:htmlpreview.github.io)"
|
|
353
|
+
"WebFetch(domain:htmlpreview.github.io)",
|
|
354
|
+
"WebFetch(domain:timbl.solidcommunity.net)"
|
|
354
355
|
]
|
|
355
356
|
}
|
|
356
357
|
}
|
package/package.json
CHANGED
|
@@ -197,10 +197,14 @@ export async function createPodStructure(name, webId, podUri, issuer, defaultQuo
|
|
|
197
197
|
const privateAcl = generatePrivateAcl(`${podUri}private/`, webId);
|
|
198
198
|
await storage.write(`${podPath}private/.acl`, serializeAcl(privateAcl));
|
|
199
199
|
|
|
200
|
-
// settings folder: owner only
|
|
200
|
+
// settings folder: owner only (contains private preferences)
|
|
201
201
|
const settingsAcl = generatePrivateAcl(`${podUri}settings/`, webId);
|
|
202
202
|
await storage.write(`${podPath}settings/.acl`, serializeAcl(settingsAcl));
|
|
203
203
|
|
|
204
|
+
// publicTypeIndex: public read, overrides the private default inherited from /settings/
|
|
205
|
+
const publicTypeIndexAcl = generateOwnerAcl(`${podUri}settings/publicTypeIndex.jsonld`, webId, false);
|
|
206
|
+
await storage.write(`${podPath}settings/publicTypeIndex.jsonld.acl`, serializeAcl(publicTypeIndexAcl));
|
|
207
|
+
|
|
204
208
|
// Inbox: owner full, public append
|
|
205
209
|
const inboxAcl = generateInboxAcl(`${podUri}inbox/`, webId);
|
|
206
210
|
await storage.write(`${podPath}inbox/.acl`, serializeAcl(inboxAcl));
|
package/src/server.js
CHANGED
|
@@ -622,6 +622,10 @@ export function createServer(options = {}) {
|
|
|
622
622
|
const settingsAcl = generatePrivateAcl(`${podUri}settings/`, webId);
|
|
623
623
|
await storage.write('/settings/.acl', serializeAcl(settingsAcl));
|
|
624
624
|
|
|
625
|
+
// publicTypeIndex: public read, overrides the private default inherited from /settings/
|
|
626
|
+
const publicTypeIndexAcl = generateOwnerAcl(`${podUri}settings/publicTypeIndex.jsonld`, webId, false);
|
|
627
|
+
await storage.write('/settings/publicTypeIndex.jsonld.acl', serializeAcl(publicTypeIndexAcl));
|
|
628
|
+
|
|
625
629
|
const inboxAcl = generateInboxAcl(`${podUri}inbox/`, webId);
|
|
626
630
|
await storage.write('/inbox/.acl', serializeAcl(inboxAcl));
|
|
627
631
|
|
package/src/utils/url.js
CHANGED
|
@@ -237,8 +237,21 @@ export function getContentType(filePath) {
|
|
|
237
237
|
'.md': 'text/markdown',
|
|
238
238
|
'.m3u': 'audio/mpegurl',
|
|
239
239
|
'.m3u8': 'application/vnd.apple.mpegurl',
|
|
240
|
-
'.pls': 'audio/x-scpls'
|
|
240
|
+
'.pls': 'audio/x-scpls',
|
|
241
|
+
// Solid ACL/meta as extensions (e.g. publicTypeIndex.jsonld.acl)
|
|
242
|
+
'.acl': 'application/ld+json',
|
|
243
|
+
'.meta': 'application/ld+json'
|
|
241
244
|
};
|
|
245
|
+
|
|
246
|
+
// Solid convention dotfiles (.acl, .meta) are RDF resources. path.extname
|
|
247
|
+
// returns '' for leading-dot names, so the map lookup above misses them;
|
|
248
|
+
// fall back to a basename check and tag them as JSON-LD — the format JSS
|
|
249
|
+
// writes them in via serializeAcl() / createPodStructure(). Content
|
|
250
|
+
// negotiation then handles Turtle-native clients (umai, Soukai-based apps,
|
|
251
|
+
// older Solid tooling) via handleGet's conneg branch.
|
|
252
|
+
const base = path.basename(filePath);
|
|
253
|
+
if (base === '.acl' || base === '.meta') return 'application/ld+json';
|
|
254
|
+
|
|
242
255
|
return types[ext] || 'application/octet-stream';
|
|
243
256
|
}
|
|
244
257
|
|
package/test/conneg.test.js
CHANGED
|
@@ -194,6 +194,73 @@ describe('Content Negotiation (conneg enabled)', () => {
|
|
|
194
194
|
'Accept-Post should include text/turtle');
|
|
195
195
|
});
|
|
196
196
|
});
|
|
197
|
+
|
|
198
|
+
// Regression coverage for #294 — Solid convention dotfiles (.acl, .meta)
|
|
199
|
+
// were excluded from conneg because getContentType() returned
|
|
200
|
+
// application/octet-stream for them. Turtle-native clients (umai etc.)
|
|
201
|
+
// fetching <container>/.meta got JSON-LD back and errored on parse.
|
|
202
|
+
describe('Solid convention dotfiles (#294)', () => {
|
|
203
|
+
const metaData = {
|
|
204
|
+
'@context': { 'ldp': 'http://www.w3.org/ns/ldp#' },
|
|
205
|
+
'@id': '',
|
|
206
|
+
'@type': 'ldp:BasicContainer'
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
before(async () => {
|
|
210
|
+
// Write a JSON-LD .meta file (the format JSS writes internally).
|
|
211
|
+
await request('/connegtest/public/.meta', {
|
|
212
|
+
method: 'PUT',
|
|
213
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
214
|
+
body: JSON.stringify(metaData),
|
|
215
|
+
auth: 'connegtest'
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('serves .meta as JSON-LD by default', async () => {
|
|
220
|
+
const res = await request('/connegtest/public/.meta', { auth: 'connegtest' });
|
|
221
|
+
assertStatus(res, 200);
|
|
222
|
+
assertHeaderContains(res, 'Content-Type', 'application/ld+json');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('serves .meta as Turtle when Accept: text/turtle (the umai case)', async () => {
|
|
226
|
+
const res = await request('/connegtest/public/.meta', {
|
|
227
|
+
headers: { 'Accept': 'text/turtle' },
|
|
228
|
+
auth: 'connegtest'
|
|
229
|
+
});
|
|
230
|
+
assertStatus(res, 200);
|
|
231
|
+
assertHeaderContains(res, 'Content-Type', 'text/turtle');
|
|
232
|
+
const turtle = await res.text();
|
|
233
|
+
// First byte after the `@prefix` block must parse as Turtle,
|
|
234
|
+
// not '{' (the bug signature umai hit).
|
|
235
|
+
assert.ok(!turtle.trimStart().startsWith('{'),
|
|
236
|
+
`response looks like JSON, not Turtle: ${turtle.slice(0, 60)}`);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('accepts Turtle PUT to .meta and round-trips to JSON-LD', async () => {
|
|
240
|
+
const turtle = `
|
|
241
|
+
@prefix ldp: <http://www.w3.org/ns/ldp#>.
|
|
242
|
+
<> a ldp:BasicContainer.
|
|
243
|
+
`;
|
|
244
|
+
const putRes = await request('/connegtest/public/.meta', {
|
|
245
|
+
method: 'PUT',
|
|
246
|
+
headers: { 'Content-Type': 'text/turtle' },
|
|
247
|
+
body: turtle,
|
|
248
|
+
auth: 'connegtest'
|
|
249
|
+
});
|
|
250
|
+
assert.ok(putRes.status < 300, `PUT turtle should succeed, got ${putRes.status}`);
|
|
251
|
+
|
|
252
|
+
// Default GET now serves the converted-and-stored JSON-LD.
|
|
253
|
+
const getRes = await request('/connegtest/public/.meta', {
|
|
254
|
+
headers: { 'Accept': 'application/ld+json' },
|
|
255
|
+
auth: 'connegtest'
|
|
256
|
+
});
|
|
257
|
+
assertStatus(getRes, 200);
|
|
258
|
+
assertHeaderContains(getRes, 'Content-Type', 'application/ld+json');
|
|
259
|
+
const body = await getRes.json();
|
|
260
|
+
assert.ok(body['@context'] || body['@graph'] || body['@type'] || body['@id'],
|
|
261
|
+
'round-tripped JSON-LD should have at least one @-keyword');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
197
264
|
});
|
|
198
265
|
|
|
199
266
|
describe('Content Negotiation (conneg disabled - default)', () => {
|
package/test/idp.test.js
CHANGED
|
@@ -409,6 +409,55 @@ describe('Identity Provider - Single-user mode landing', () => {
|
|
|
409
409
|
});
|
|
410
410
|
});
|
|
411
411
|
|
|
412
|
+
// Root-level pod (singleUserName: '/') — verifies createRootPodStructure wires
|
|
413
|
+
// publicTypeIndex as public-read and privateTypeIndex as owner-only.
|
|
414
|
+
// Regression coverage for #297.
|
|
415
|
+
describe('Identity Provider - Root pod type index ACLs', () => {
|
|
416
|
+
let server;
|
|
417
|
+
let baseUrl;
|
|
418
|
+
const ROOT_POD_DATA_DIR = './test-data-idp-root-pod';
|
|
419
|
+
|
|
420
|
+
before(async () => {
|
|
421
|
+
await fs.remove(ROOT_POD_DATA_DIR);
|
|
422
|
+
await fs.ensureDir(ROOT_POD_DATA_DIR);
|
|
423
|
+
|
|
424
|
+
const port = await getAvailablePort();
|
|
425
|
+
baseUrl = `http://${TEST_HOST}:${port}`;
|
|
426
|
+
|
|
427
|
+
server = createServer({
|
|
428
|
+
logger: false,
|
|
429
|
+
root: ROOT_POD_DATA_DIR,
|
|
430
|
+
idp: true,
|
|
431
|
+
idpIssuer: baseUrl,
|
|
432
|
+
singleUser: true,
|
|
433
|
+
singleUserName: '/',
|
|
434
|
+
forceCloseConnections: true,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
await server.listen({ port, host: TEST_HOST });
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
after(async () => {
|
|
441
|
+
await server.close();
|
|
442
|
+
await fs.remove(ROOT_POD_DATA_DIR);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('publicTypeIndex is readable without auth', async () => {
|
|
446
|
+
const res = await fetch(`${baseUrl}/settings/publicTypeIndex.jsonld`);
|
|
447
|
+
assert.strictEqual(res.status, 200);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('privateTypeIndex requires auth', async () => {
|
|
451
|
+
const res = await fetch(`${baseUrl}/settings/privateTypeIndex.jsonld`);
|
|
452
|
+
assert.strictEqual(res.status, 401);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('prefs requires auth', async () => {
|
|
456
|
+
const res = await fetch(`${baseUrl}/settings/prefs.jsonld`);
|
|
457
|
+
assert.strictEqual(res.status, 401);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
412
461
|
describe('Identity Provider - Accounts', () => {
|
|
413
462
|
let server;
|
|
414
463
|
let accountsUrl;
|
package/test/pod.test.js
CHANGED
|
@@ -115,5 +115,21 @@ describe('Pod Lifecycle', () => {
|
|
|
115
115
|
const privIndex = await request('/dan/settings/privateTypeIndex.jsonld', { auth: 'dan' });
|
|
116
116
|
assertStatus(privIndex, 200);
|
|
117
117
|
});
|
|
118
|
+
|
|
119
|
+
it('should make publicTypeIndex publicly readable but keep privateTypeIndex private', async () => {
|
|
120
|
+
await createTestPod('elsa');
|
|
121
|
+
|
|
122
|
+
// publicTypeIndex: no auth required (per Solid Type Indexes spec)
|
|
123
|
+
const pubIndex = await request('/elsa/settings/publicTypeIndex.jsonld');
|
|
124
|
+
assertStatus(pubIndex, 200);
|
|
125
|
+
|
|
126
|
+
// privateTypeIndex: auth required
|
|
127
|
+
const privIndex = await request('/elsa/settings/privateTypeIndex.jsonld');
|
|
128
|
+
assertStatus(privIndex, 401);
|
|
129
|
+
|
|
130
|
+
// prefs: auth required (private by inheritance from /settings/)
|
|
131
|
+
const prefs = await request('/elsa/settings/prefs.jsonld');
|
|
132
|
+
assertStatus(prefs, 401);
|
|
133
|
+
});
|
|
118
134
|
});
|
|
119
135
|
});
|
package/test/url.test.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { describe, it } from 'node:test';
|
|
9
9
|
import assert from 'node:assert';
|
|
10
|
-
import { getPodName } from '../src/utils/url.js';
|
|
10
|
+
import { getPodName, getContentType } from '../src/utils/url.js';
|
|
11
11
|
|
|
12
12
|
describe('getPodName', () => {
|
|
13
13
|
describe('subdomain mode', () => {
|
|
@@ -73,3 +73,48 @@ describe('getPodName', () => {
|
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
// Regression coverage for #294 — .acl and .meta must be recognised as RDF
|
|
78
|
+
// resources so content negotiation kicks in for Turtle-native clients.
|
|
79
|
+
describe('getContentType', () => {
|
|
80
|
+
describe('extension-based mapping (existing)', () => {
|
|
81
|
+
it('maps .jsonld → application/ld+json', () => {
|
|
82
|
+
assert.strictEqual(getContentType('/x/card.jsonld'), 'application/ld+json');
|
|
83
|
+
});
|
|
84
|
+
it('maps .ttl → text/turtle', () => {
|
|
85
|
+
assert.strictEqual(getContentType('/x/card.ttl'), 'text/turtle');
|
|
86
|
+
});
|
|
87
|
+
it('falls back to application/octet-stream for unknown extensions', () => {
|
|
88
|
+
assert.strictEqual(getContentType('/x/file.xyz'), 'application/octet-stream');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('Solid convention dotfiles (#294)', () => {
|
|
93
|
+
it('treats .acl as application/ld+json (the format JSS writes it in)', () => {
|
|
94
|
+
assert.strictEqual(getContentType('/alice/public/.acl'), 'application/ld+json');
|
|
95
|
+
assert.strictEqual(getContentType('.acl'), 'application/ld+json');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('treats .meta as application/ld+json', () => {
|
|
99
|
+
assert.strictEqual(getContentType('/alice/public/.meta'), 'application/ld+json');
|
|
100
|
+
assert.strictEqual(getContentType('.meta'), 'application/ld+json');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('does not mistake non-dotfile paths containing .acl for ACL files', () => {
|
|
104
|
+
// A regular file that happens to have "acl" in its name/path stays
|
|
105
|
+
// classified by extension, not by coincidence.
|
|
106
|
+
assert.strictEqual(getContentType('/alice/notes/my-acl-plan.md'), 'text/markdown');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('.acl / .meta as extensions (#297)', () => {
|
|
111
|
+
it('treats *.acl (extension) as application/ld+json', () => {
|
|
112
|
+
assert.strictEqual(getContentType('/settings/publicTypeIndex.jsonld.acl'), 'application/ld+json');
|
|
113
|
+
assert.strictEqual(getContentType('/alice/private/secret.json.acl'), 'application/ld+json');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('treats *.meta (extension) as application/ld+json', () => {
|
|
117
|
+
assert.strictEqual(getContentType('/alice/resource.meta'), 'application/ld+json');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|