javascript-solid-server 0.0.109 → 0.0.111
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/README.md +56 -1349
- package/docs/activitypub.md +109 -0
- package/docs/architecture.md +165 -0
- package/docs/authentication.md +157 -0
- package/docs/configuration.md +471 -0
- package/docs/mongodb.md +42 -0
- package/docs/payments.md +94 -0
- package/docs/remotestorage.md +86 -0
- package/docs/security.md +96 -0
- package/docs/webrtc.md +66 -0
- package/package.json +1 -1
- package/src/patch/n3-patch.js +40 -23
- package/test/patch.test.js +61 -0
package/docs/security.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
## Security
|
|
2
|
+
|
|
3
|
+
### Root ACL Required
|
|
4
|
+
|
|
5
|
+
JSS uses **restrictive mode** by default: if no ACL file exists for a resource, access is denied. This prevents unauthorized writes to unprotected containers.
|
|
6
|
+
|
|
7
|
+
**You must create a root `.acl` file** in your data directory. Example (JSON-LD format):
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"@context": {
|
|
12
|
+
"acl": "http://www.w3.org/ns/auth/acl#",
|
|
13
|
+
"foaf": "http://xmlns.com/foaf/0.1/"
|
|
14
|
+
},
|
|
15
|
+
"@graph": [
|
|
16
|
+
{
|
|
17
|
+
"@id": "#owner",
|
|
18
|
+
"@type": "acl:Authorization",
|
|
19
|
+
"acl:agent": { "@id": "https://your-domain.com/profile/card#me" },
|
|
20
|
+
"acl:accessTo": { "@id": "https://your-domain.com/" },
|
|
21
|
+
"acl:default": { "@id": "https://your-domain.com/" },
|
|
22
|
+
"acl:mode": [
|
|
23
|
+
{ "@id": "acl:Read" },
|
|
24
|
+
{ "@id": "acl:Write" },
|
|
25
|
+
{ "@id": "acl:Control" }
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"@id": "#public",
|
|
30
|
+
"@type": "acl:Authorization",
|
|
31
|
+
"acl:agentClass": { "@id": "foaf:Agent" },
|
|
32
|
+
"acl:accessTo": { "@id": "https://your-domain.com/" },
|
|
33
|
+
"acl:default": { "@id": "https://your-domain.com/" },
|
|
34
|
+
"acl:mode": [
|
|
35
|
+
{ "@id": "acl:Read" }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Save this as `data/.acl` (replacing `your-domain.com` with your actual domain).
|
|
43
|
+
|
|
44
|
+
See [Issue #32](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/32) for background.
|
|
45
|
+
|
|
46
|
+
## Subdomain Mode (XSS Protection)
|
|
47
|
+
|
|
48
|
+
By default, JSS uses **path-based pods** (`/alice/`, `/bob/`). This is simple but has a security limitation: all pods share the same origin, making cross-site scripting (XSS) attacks possible between pods.
|
|
49
|
+
|
|
50
|
+
**Subdomain mode** provides **origin isolation** - each pod gets its own subdomain (`alice.example.com`, `bob.example.com`), preventing XSS attacks between pods.
|
|
51
|
+
|
|
52
|
+
### Why Subdomain Mode?
|
|
53
|
+
|
|
54
|
+
| Mode | URL | Origin | XSS Risk |
|
|
55
|
+
|------|-----|--------|----------|
|
|
56
|
+
| Path-based | `example.com/alice/` | `example.com` | Shared origin - pods can XSS each other |
|
|
57
|
+
| Subdomain | `alice.example.com/` | `alice.example.com` | Isolated - browser's Same-Origin Policy protects |
|
|
58
|
+
|
|
59
|
+
### Enabling Subdomain Mode
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
jss start --subdomains --base-domain example.com
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or via environment variables:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
export JSS_SUBDOMAINS=true
|
|
69
|
+
export JSS_BASE_DOMAIN=example.com
|
|
70
|
+
jss start
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### DNS Configuration
|
|
74
|
+
|
|
75
|
+
You need a **wildcard DNS record** pointing to your server:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
*.example.com A <your-server-ip>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Pod URLs in Subdomain Mode
|
|
82
|
+
|
|
83
|
+
| Path Mode | Subdomain Mode |
|
|
84
|
+
|-----------|----------------|
|
|
85
|
+
| `example.com/alice/` | `alice.example.com/` |
|
|
86
|
+
| `example.com/alice/public/file.txt` | `alice.example.com/public/file.txt` |
|
|
87
|
+
| `example.com/alice/#me` | `alice.example.com/#me` |
|
|
88
|
+
|
|
89
|
+
Pod creation still uses the main domain:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
curl -X POST https://example.com/.pods \
|
|
93
|
+
-H "Content-Type: application/json" \
|
|
94
|
+
-d '{"name": "alice"}'
|
|
95
|
+
```
|
|
96
|
+
|
package/docs/webrtc.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
## WebRTC Signaling
|
|
2
|
+
|
|
3
|
+
Peer-to-peer communication via WebRTC, using JSS as the signaling server. Once peers are connected, all media and data flows directly between them.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
jss start --webrtc
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
### How It Works
|
|
10
|
+
|
|
11
|
+
1. Both peers connect to `wss://your.pod/.webrtc` (WebID auth required)
|
|
12
|
+
2. Caller sends an SDP offer targeting the callee's WebID
|
|
13
|
+
3. JSS relays the offer/answer and ICE candidates between peers
|
|
14
|
+
4. Once a direct path is found, the peer-to-peer connection is established
|
|
15
|
+
5. JSS steps out — video, audio, files, and data flow directly between peers
|
|
16
|
+
|
|
17
|
+
### Protocol
|
|
18
|
+
|
|
19
|
+
Messages are JSON over WebSocket:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
// Send an offer to another user
|
|
23
|
+
{ "type": "offer", "to": "https://bob.example/profile/card#me", "sdp": "..." }
|
|
24
|
+
|
|
25
|
+
// Receive an offer from another user
|
|
26
|
+
{ "type": "offer", "from": "https://alice.example/profile/card#me", "sdp": "..." }
|
|
27
|
+
|
|
28
|
+
// ICE candidate exchange
|
|
29
|
+
{ "type": "candidate", "to": "https://bob.example/profile/card#me", "candidate": {...} }
|
|
30
|
+
|
|
31
|
+
// Hang up
|
|
32
|
+
{ "type": "hangup", "to": "https://bob.example/profile/card#me" }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
On connect, peers receive a list of online users and get notified when others join or leave.
|
|
36
|
+
|
|
37
|
+
## Tunnel Proxy (Decentralized ngrok)
|
|
38
|
+
|
|
39
|
+
Expose a local dev server to the internet through your JSS pod. A tunnel client connects via WebSocket, registers a name, and receives proxied HTTP requests.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
jss start --tunnel
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### How It Works
|
|
46
|
+
|
|
47
|
+
1. Tunnel client connects to `wss://your.pod/.tunnel` (WebID auth required)
|
|
48
|
+
2. Client registers a name: `{ "type": "register", "name": "myapp" }`
|
|
49
|
+
3. Public URL becomes available at `https://your.pod/tunnel/myapp/`
|
|
50
|
+
4. HTTP requests to that URL are serialized and sent to the tunnel client over WebSocket
|
|
51
|
+
5. Tunnel client forwards to localhost, returns the response
|
|
52
|
+
|
|
53
|
+
### Tunnel Client Protocol
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
// 1. Register a tunnel
|
|
57
|
+
→ { "type": "register", "name": "myapp" }
|
|
58
|
+
← { "type": "registered", "name": "myapp", "url": "/tunnel/myapp/" }
|
|
59
|
+
|
|
60
|
+
// 2. Receive proxied HTTP requests
|
|
61
|
+
← { "type": "request", "id": "uuid", "method": "GET", "path": "/api/hello", "headers": {...} }
|
|
62
|
+
|
|
63
|
+
// 3. Return the response
|
|
64
|
+
→ { "type": "response", "id": "uuid", "status": 200, "headers": {...}, "body": "..." }
|
|
65
|
+
```
|
|
66
|
+
|
package/package.json
CHANGED
package/src/patch/n3-patch.js
CHANGED
|
@@ -57,6 +57,7 @@ export function parseN3Patch(patchText, baseUri) {
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Parse triples from N3 block content
|
|
60
|
+
* Handles Turtle semicolon shorthand (same subject, different predicate-object)
|
|
60
61
|
*/
|
|
61
62
|
function parseTriples(content, prefixes, baseUri) {
|
|
62
63
|
const triples = [];
|
|
@@ -68,11 +69,37 @@ function parseTriples(content, prefixes, baseUri) {
|
|
|
68
69
|
// Split by '.' but be careful with strings containing '.'
|
|
69
70
|
const statements = splitStatements(content);
|
|
70
71
|
|
|
72
|
+
let lastSubject = null;
|
|
71
73
|
for (const stmt of statements) {
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
74
|
+
const trimmed = stmt.trim();
|
|
75
|
+
if (!trimmed) continue;
|
|
76
|
+
|
|
77
|
+
const tokens = tokenize(trimmed);
|
|
78
|
+
if (tokens.length < 2) continue;
|
|
79
|
+
|
|
80
|
+
let subject, predicate, object;
|
|
81
|
+
|
|
82
|
+
if (tokens.length >= 3) {
|
|
83
|
+
// Full triple: subject predicate object
|
|
84
|
+
subject = resolveValue(tokens[0], prefixes, baseUri);
|
|
85
|
+
predicate = resolveValue(tokens[1], prefixes, baseUri);
|
|
86
|
+
object = resolveValue(tokens.slice(2).join(' '), prefixes, baseUri);
|
|
87
|
+
lastSubject = subject;
|
|
88
|
+
} else if (tokens.length === 2 && lastSubject) {
|
|
89
|
+
// Semicolon continuation: predicate object (reuse last subject)
|
|
90
|
+
subject = lastSubject;
|
|
91
|
+
predicate = resolveValue(tokens[0], prefixes, baseUri);
|
|
92
|
+
object = resolveValue(tokens[1], prefixes, baseUri);
|
|
93
|
+
} else {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle 'a' as rdf:type
|
|
98
|
+
if (predicate === 'a') {
|
|
99
|
+
predicate = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
75
100
|
}
|
|
101
|
+
|
|
102
|
+
triples.push({ subject, predicate, object });
|
|
76
103
|
}
|
|
77
104
|
|
|
78
105
|
return triples;
|
|
@@ -86,11 +113,18 @@ function splitStatements(content) {
|
|
|
86
113
|
let current = '';
|
|
87
114
|
let inString = false;
|
|
88
115
|
let stringChar = null;
|
|
116
|
+
let inIri = false;
|
|
89
117
|
|
|
90
118
|
for (let i = 0; i < content.length; i++) {
|
|
91
119
|
const char = content[i];
|
|
92
120
|
|
|
93
|
-
if (!inString &&
|
|
121
|
+
if (!inString && !inIri && char === '<') {
|
|
122
|
+
inIri = true;
|
|
123
|
+
current += char;
|
|
124
|
+
} else if (inIri && char === '>') {
|
|
125
|
+
inIri = false;
|
|
126
|
+
current += char;
|
|
127
|
+
} else if (!inIri && !inString && (char === '"' || char === "'")) {
|
|
94
128
|
inString = true;
|
|
95
129
|
stringChar = char;
|
|
96
130
|
current += char;
|
|
@@ -98,12 +132,12 @@ function splitStatements(content) {
|
|
|
98
132
|
inString = false;
|
|
99
133
|
stringChar = null;
|
|
100
134
|
current += char;
|
|
101
|
-
} else if (!inString && char === '.') {
|
|
135
|
+
} else if (!inString && !inIri && char === '.') {
|
|
102
136
|
if (current.trim()) {
|
|
103
137
|
statements.push(current);
|
|
104
138
|
}
|
|
105
139
|
current = '';
|
|
106
|
-
} else if (!inString && char === ';') {
|
|
140
|
+
} else if (!inString && !inIri && char === ';') {
|
|
107
141
|
// Turtle shorthand - same subject, different predicate
|
|
108
142
|
if (current.trim()) {
|
|
109
143
|
statements.push(current);
|
|
@@ -121,23 +155,6 @@ function splitStatements(content) {
|
|
|
121
155
|
return statements;
|
|
122
156
|
}
|
|
123
157
|
|
|
124
|
-
/**
|
|
125
|
-
* Parse a single N3 statement into a triple
|
|
126
|
-
*/
|
|
127
|
-
function parseStatement(stmt, prefixes, baseUri) {
|
|
128
|
-
if (!stmt) return null;
|
|
129
|
-
|
|
130
|
-
// Tokenize - split by whitespace but respect quotes
|
|
131
|
-
const tokens = tokenize(stmt);
|
|
132
|
-
if (tokens.length < 3) return null;
|
|
133
|
-
|
|
134
|
-
const subject = resolveValue(tokens[0], prefixes, baseUri);
|
|
135
|
-
const predicate = resolveValue(tokens[1], prefixes, baseUri);
|
|
136
|
-
const object = resolveValue(tokens.slice(2).join(' '), prefixes, baseUri);
|
|
137
|
-
|
|
138
|
-
return { subject, predicate, object };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
158
|
/**
|
|
142
159
|
* Tokenize a statement respecting quoted strings
|
|
143
160
|
*/
|
package/test/patch.test.js
CHANGED
|
@@ -185,6 +185,67 @@ describe('PATCH Operations', () => {
|
|
|
185
185
|
assert.ok(data['@graph'], 'Should have @graph');
|
|
186
186
|
assert.strictEqual(data['@graph'].length, 2, 'Should have 2 nodes');
|
|
187
187
|
});
|
|
188
|
+
|
|
189
|
+
it('should handle semicolon shorthand and rdf:type "a" keyword', async () => {
|
|
190
|
+
// Create initial resource with @graph
|
|
191
|
+
const initial = {
|
|
192
|
+
'@context': { 'solid': 'http://www.w3.org/ns/solid/terms#' },
|
|
193
|
+
'@graph': []
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
await request('/patchtest/public/patch-semicolon.json', {
|
|
197
|
+
method: 'PUT',
|
|
198
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
199
|
+
body: JSON.stringify(initial),
|
|
200
|
+
auth: 'patchtest'
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Use semicolons and 'a' keyword (Turtle shorthand)
|
|
204
|
+
const patch = `
|
|
205
|
+
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
|
|
206
|
+
@prefix wf: <http://www.w3.org/2005/01/wf/flow#>.
|
|
207
|
+
_:patch a solid:InsertDeletePatch;
|
|
208
|
+
solid:inserts {
|
|
209
|
+
<#reg1> a solid:TypeRegistration;
|
|
210
|
+
solid:forClass wf:Tracker;
|
|
211
|
+
solid:instance <https://example.com/todo/data.jsonld#this>.
|
|
212
|
+
}.
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
const res = await request('/patchtest/public/patch-semicolon.json', {
|
|
216
|
+
method: 'PATCH',
|
|
217
|
+
headers: { 'Content-Type': 'text/n3' },
|
|
218
|
+
body: patch,
|
|
219
|
+
auth: 'patchtest'
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
assertStatus(res, 204);
|
|
223
|
+
|
|
224
|
+
// Verify all three triples were inserted
|
|
225
|
+
const verify = await request('/patchtest/public/patch-semicolon.json');
|
|
226
|
+
const data = await verify.json();
|
|
227
|
+
const node = data['@graph'].find(n => n['@id'] && n['@id'].includes('#reg1'));
|
|
228
|
+
assert.ok(node, 'Should have the reg1 node');
|
|
229
|
+
|
|
230
|
+
// Check rdf:type value (from 'a' keyword)
|
|
231
|
+
const rdfType = node['rdf:type'] || node['http://www.w3.org/1999/02/22-rdf-syntax-ns#type'];
|
|
232
|
+
assert.ok(rdfType, 'Should have rdf:type (from "a" keyword)');
|
|
233
|
+
const typeId = rdfType['@id'] || rdfType;
|
|
234
|
+
assert.ok(String(typeId).includes('TypeRegistration'), `rdf:type should be TypeRegistration, got ${typeId}`);
|
|
235
|
+
|
|
236
|
+
// Check solid:forClass value
|
|
237
|
+
const forClass = node['solid:forClass'];
|
|
238
|
+
assert.ok(forClass, 'Should have solid:forClass');
|
|
239
|
+
const forClassId = forClass['@id'] || forClass;
|
|
240
|
+
assert.ok(String(forClassId).includes('Tracker'), `solid:forClass should be Tracker, got ${forClassId}`);
|
|
241
|
+
|
|
242
|
+
// Check solid:instance value (contains a dot in the IRI - tests IRI splitting)
|
|
243
|
+
const instance = node['solid:instance'];
|
|
244
|
+
assert.ok(instance, 'Should have solid:instance');
|
|
245
|
+
const instanceId = instance['@id'] || instance;
|
|
246
|
+
assert.strictEqual(instanceId, 'https://example.com/todo/data.jsonld#this',
|
|
247
|
+
'solid:instance should have full IRI preserved');
|
|
248
|
+
});
|
|
188
249
|
});
|
|
189
250
|
|
|
190
251
|
describe('PATCH Error Handling', () => {
|