javascript-solid-server 0.0.110 → 0.0.112
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/README.md +70 -1358
- package/bin/jss.js +1 -5
- package/docs/activitypub.md +109 -0
- package/docs/architecture.md +165 -0
- package/docs/authentication.md +157 -0
- package/docs/configuration.md +434 -0
- package/docs/invites.md +43 -0
- package/docs/mashlib.md +58 -0
- package/docs/mongodb.md +42 -0
- package/docs/nostr.md +56 -0
- package/docs/notifications.md +50 -0
- package/docs/payments.md +94 -0
- package/docs/quotas.md +36 -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/auth/middleware.js +4 -7
- package/src/config.js +1 -6
- package/src/handlers/resource.js +7 -13
- package/src/server.js +11 -74
package/docs/payments.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
## HTTP 402 Paid Access
|
|
2
|
+
|
|
3
|
+
Monetize API endpoints with per-request satoshi payments. Resources under `/pay/*` require NIP-98 authentication and a positive balance.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
jss start --pay --pay-cost 10 --pay-address your-address --pay-token PODS --pay-rate 10
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
### Routes
|
|
10
|
+
|
|
11
|
+
| Method | Path | Description |
|
|
12
|
+
|--------|------|-------------|
|
|
13
|
+
| GET | `/pay/.info` | Public: cost, token info, chains, pool |
|
|
14
|
+
| GET | `/pay/.balance` | Check your balance (NIP-98 auth) |
|
|
15
|
+
| POST | `/pay/.deposit` | Deposit sats via TXO URI or MRC20 state proof |
|
|
16
|
+
| POST | `/pay/.buy` | Buy tokens with sat balance (requires `--pay-token`) |
|
|
17
|
+
| POST | `/pay/.withdraw` | Withdraw balance as portable tokens (requires `--pay-token`) |
|
|
18
|
+
| GET | `/pay/.offers` | List open sell orders (secondary market) |
|
|
19
|
+
| POST | `/pay/.sell` | Create a sell order (requires `--pay-token`) |
|
|
20
|
+
| POST | `/pay/.swap` | Execute a swap against a sell order |
|
|
21
|
+
| GET | `/pay/.pool` | AMM pool state (requires `--pay-chains`) |
|
|
22
|
+
| POST | `/pay/.pool` | AMM swap, add/remove liquidity |
|
|
23
|
+
| GET | `/pay/*` | Paid resource access (deducts balance) |
|
|
24
|
+
|
|
25
|
+
### How It Works
|
|
26
|
+
|
|
27
|
+
1. Authenticate with NIP-98 (Nostr HTTP Auth)
|
|
28
|
+
2. Check balance at `/pay/.balance`
|
|
29
|
+
3. Deposit sats by POSTing a TXO URI to `/pay/.deposit`
|
|
30
|
+
4. Access paid resources — each request deducts the configured cost
|
|
31
|
+
5. Optionally buy tokens (`/pay/.buy`) or withdraw as portable tokens (`/pay/.withdraw`)
|
|
32
|
+
6. Balance tracked in a [Web Ledger](https://webledgers.org/) at `/.well-known/webledgers/webledgers.json`
|
|
33
|
+
|
|
34
|
+
### Example
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Check balance
|
|
38
|
+
curl -H "Authorization: Nostr <base64-event>" http://localhost:3000/pay/.balance
|
|
39
|
+
|
|
40
|
+
# Deposit (post a confirmed transaction output)
|
|
41
|
+
curl -X POST -H "Authorization: Nostr <base64-event>" \
|
|
42
|
+
http://localhost:3000/pay/.deposit \
|
|
43
|
+
-d "txid:vout"
|
|
44
|
+
|
|
45
|
+
# Access paid resource
|
|
46
|
+
curl -H "Authorization: Nostr <base64-event>" http://localhost:3000/pay/my-resource
|
|
47
|
+
|
|
48
|
+
# Buy tokens with sat balance
|
|
49
|
+
curl -X POST -H "Authorization: Nostr <base64-event>" \
|
|
50
|
+
-H "Content-Type: application/json" \
|
|
51
|
+
http://localhost:3000/pay/.buy \
|
|
52
|
+
-d '{"amount": 100}'
|
|
53
|
+
|
|
54
|
+
# Withdraw entire balance as portable tokens
|
|
55
|
+
curl -X POST -H "Authorization: Nostr <base64-event>" \
|
|
56
|
+
-H "Content-Type: application/json" \
|
|
57
|
+
http://localhost:3000/pay/.withdraw \
|
|
58
|
+
-d '{"all": true}'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Deposit verification uses the mempool API (default: testnet4). The `X-Balance` and `X-Cost` headers are returned on successful paid requests. Buy and withdraw return portable MRC20 proofs with Bitcoin anchor data for independent verification.
|
|
62
|
+
|
|
63
|
+
### Secondary Market
|
|
64
|
+
|
|
65
|
+
Users can trade tokens peer-to-peer through the pod. Sell orders are created via `/pay/.sell` and filled via `/pay/.swap`. The pod acts as escrow — transferring tokens on the Bitcoin-anchored MRC20 trail and settling sats in the webledger.
|
|
66
|
+
|
|
67
|
+
### Multi-Chain AMM
|
|
68
|
+
|
|
69
|
+
Enable multi-chain deposits and an automated market maker:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
jss start --pay --pay-chains "tbtc3,tbtc4"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Deposits detect the chain from the TXO URI prefix (`txo:tbtc3:txid:vout`). Each chain's balance is tracked separately. The AMM uses a constant-product formula (x × y = k) with a 0.3% fee.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Add liquidity
|
|
79
|
+
curl -X POST -H "Authorization: Nostr <token>" \
|
|
80
|
+
-H "Content-Type: application/json" \
|
|
81
|
+
http://localhost:3000/pay/.pool \
|
|
82
|
+
-d '{"action": "add-liquidity", "tbtc3": 1000, "tbtc4": 5000}'
|
|
83
|
+
|
|
84
|
+
# Swap
|
|
85
|
+
curl -X POST -H "Authorization: Nostr <token>" \
|
|
86
|
+
-H "Content-Type: application/json" \
|
|
87
|
+
http://localhost:3000/pay/.pool \
|
|
88
|
+
-d '{"action": "swap", "sell": "tbtc3", "amount": 100}'
|
|
89
|
+
|
|
90
|
+
# Check pool state
|
|
91
|
+
curl http://localhost:3000/pay/.pool
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Supported chains: `btc`, `tbtc3`, `tbtc4`, `ltc`, `signet`.
|
package/docs/quotas.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Storage Quotas
|
|
2
|
+
|
|
3
|
+
Limit storage per pod to prevent abuse and manage resources.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
jss start --default-quota 50MB
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Managing Quotas
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Set quota for a user (overrides default)
|
|
13
|
+
jss quota set alice 100MB
|
|
14
|
+
|
|
15
|
+
# Show quota info
|
|
16
|
+
jss quota show alice
|
|
17
|
+
# alice:
|
|
18
|
+
# Used: 12.5 MB
|
|
19
|
+
# Limit: 100 MB
|
|
20
|
+
# Free: 87.5 MB
|
|
21
|
+
# Usage: 12%
|
|
22
|
+
|
|
23
|
+
# Recalculate from actual disk usage
|
|
24
|
+
jss quota reconcile alice
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How It Works
|
|
28
|
+
|
|
29
|
+
- Quotas are tracked incrementally on PUT, POST, and DELETE operations
|
|
30
|
+
- When quota is exceeded, the server returns HTTP 507 Insufficient Storage
|
|
31
|
+
- Each pod stores its quota in `/{pod}/.quota.json`
|
|
32
|
+
- Use `reconcile` to fix quota drift from manual file changes
|
|
33
|
+
|
|
34
|
+
## Size Formats
|
|
35
|
+
|
|
36
|
+
Supported formats: `50MB`, `1GB`, `500KB`, `1TB`
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
## remoteStorage
|
|
2
|
+
|
|
3
|
+
JSS implements the [remoteStorage protocol](https://remotestorage.io/spec/draft-dejong-remotestorage-22). The storage routes are always available, but WebFinger discovery and OAuth require `--activitypub` (which provides the WebFinger and OAuth endpoints). Any remoteStorage-compatible app can store and sync data on your pod.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
jss start --activitypub --idp
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
### Discovery
|
|
10
|
+
|
|
11
|
+
remoteStorage clients discover the storage endpoint via WebFinger:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
curl "http://localhost:3000/.well-known/webfinger?resource=acct:me@localhost:3000"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The response includes a `remotestorage` link relation pointing to `/storage/me/`.
|
|
18
|
+
|
|
19
|
+
### Endpoints
|
|
20
|
+
|
|
21
|
+
| Method | Endpoint | Description |
|
|
22
|
+
|--------|----------|-------------|
|
|
23
|
+
| `GET` | `/storage/:user/*` | Read file or list folder (JSON-LD) |
|
|
24
|
+
| `HEAD` | `/storage/:user/*` | Get metadata (ETag, Content-Type, size) |
|
|
25
|
+
| `PUT` | `/storage/:user/*` | Write file (creates parent folders) |
|
|
26
|
+
| `DELETE` | `/storage/:user/*` | Delete file |
|
|
27
|
+
|
|
28
|
+
### How It Works
|
|
29
|
+
|
|
30
|
+
- **Auth**: Bearer token via OAuth 2.0 (same flow as Mastodon clients)
|
|
31
|
+
- **Public folder**: `/storage/me/public/*` is readable without auth
|
|
32
|
+
- **Conditional requests**: If-Match, If-None-Match (uses shared ETag utilities)
|
|
33
|
+
- **Dotfile protection**: `.acl`, `.meta`, and other dotfiles are blocked
|
|
34
|
+
- **Read-only mode**: Respects `--read-only` flag
|
|
35
|
+
- **Streaming**: Large files are streamed, not buffered
|
|
36
|
+
|
|
37
|
+
### Testing
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Write a file (needs Bearer token from OAuth flow)
|
|
41
|
+
curl -X PUT http://localhost:3000/storage/me/documents/hello.txt \
|
|
42
|
+
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
43
|
+
-H "Content-Type: text/plain" \
|
|
44
|
+
-d "Hello, remoteStorage!"
|
|
45
|
+
|
|
46
|
+
# Read it back
|
|
47
|
+
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
|
48
|
+
http://localhost:3000/storage/me/documents/hello.txt
|
|
49
|
+
|
|
50
|
+
# List a folder
|
|
51
|
+
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
|
52
|
+
http://localhost:3000/storage/me/documents/
|
|
53
|
+
|
|
54
|
+
# Read from public folder (no auth needed)
|
|
55
|
+
curl http://localhost:3000/storage/me/public/readme.txt
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Linking Nostr to WebID (did:nostr)
|
|
59
|
+
|
|
60
|
+
Bridge your Nostr identity to a Solid WebID for seamless authentication:
|
|
61
|
+
|
|
62
|
+
**Step 1:** Add your WebID to your Nostr profile (kind 0 event):
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"name": "alice",
|
|
66
|
+
"alsoKnownAs": ["https://solid.social/alice/profile/card#me"]
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Step 2:** Add the did:nostr link to your WebID profile:
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"@id": "#me",
|
|
74
|
+
"owl:sameAs": "did:nostr:<your-64-char-hex-pubkey>"
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**How it works:**
|
|
79
|
+
1. NIP-98 signature is verified (existing flow)
|
|
80
|
+
2. DID document is fetched from `nostr.social/.well-known/did/nostr/<pubkey>.json`
|
|
81
|
+
3. `alsoKnownAs` is checked for a WebID URL
|
|
82
|
+
4. WebID profile is fetched and `owl:sameAs` verified
|
|
83
|
+
5. If bidirectional link exists → authenticated as WebID
|
|
84
|
+
|
|
85
|
+
This enables Nostr users to access their Solid pods using existing NIP-07 browser extensions.
|
|
86
|
+
|
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/auth/middleware.js
CHANGED
|
@@ -9,7 +9,7 @@ import { checkAccess, getRequiredMode } from '../wac/checker.js';
|
|
|
9
9
|
import { AccessMode } from '../wac/parser.js';
|
|
10
10
|
import * as storage from '../storage/filesystem.js';
|
|
11
11
|
import { getEffectiveUrlPath } from '../utils/url.js';
|
|
12
|
-
import { generateDatabrowserHtml, generateModuleDatabrowserHtml
|
|
12
|
+
import { generateDatabrowserHtml, generateModuleDatabrowserHtml } from '../mashlib/index.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Build a resource URL for WAC checking, normalizing path-based pod access
|
|
@@ -148,12 +148,9 @@ export function handleUnauthorized(request, reply, isAuthenticated, wacAllow, au
|
|
|
148
148
|
// If mashlib is enabled, serve mashlib instead of static error page
|
|
149
149
|
// Mashlib has built-in login functionality via panes.runDataBrowser()
|
|
150
150
|
if (request.mashlibEnabled) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
?
|
|
154
|
-
: request.mashlibModule
|
|
155
|
-
? generateModuleDatabrowserHtml(request.mashlibModule)
|
|
156
|
-
: generateDatabrowserHtml(request.url, request.mashlibCdn ? request.mashlibVersion : null);
|
|
151
|
+
const html = request.mashlibModule
|
|
152
|
+
? generateModuleDatabrowserHtml(request.mashlibModule)
|
|
153
|
+
: generateDatabrowserHtml(request.url, request.mashlibCdn ? request.mashlibVersion : null);
|
|
157
154
|
return reply.code(statusCode).type('text/html').send(html);
|
|
158
155
|
}
|
|
159
156
|
return reply.code(statusCode).type('text/html').send(getErrorPage(statusCode, isAuthenticated, request));
|
package/src/config.js
CHANGED
|
@@ -43,9 +43,6 @@ export const defaults = {
|
|
|
43
43
|
mashlibVersion: '2.0.0',
|
|
44
44
|
mashlibModule: false,
|
|
45
45
|
|
|
46
|
-
// SolidOS UI (modern Nextcloud-style interface)
|
|
47
|
-
solidosUi: false,
|
|
48
|
-
|
|
49
46
|
// Git HTTP backend
|
|
50
47
|
git: false,
|
|
51
48
|
|
|
@@ -137,7 +134,6 @@ const envMap = {
|
|
|
137
134
|
JSS_MASHLIB_CDN: 'mashlibCdn',
|
|
138
135
|
JSS_MASHLIB_VERSION: 'mashlibVersion',
|
|
139
136
|
JSS_MASHLIB_MODULE: 'mashlibModule',
|
|
140
|
-
JSS_SOLIDOS_UI: 'solidosUi',
|
|
141
137
|
JSS_GIT: 'git',
|
|
142
138
|
JSS_NOSTR: 'nostr',
|
|
143
139
|
JSS_NOSTR_PATH: 'nostrPath',
|
|
@@ -331,8 +327,7 @@ export function printConfig(config) {
|
|
|
331
327
|
console.log(` Notifications: ${config.notifications}`);
|
|
332
328
|
console.log(` IdP: ${config.idp ? (config.idpIssuer || 'enabled') : 'disabled'}`);
|
|
333
329
|
console.log(` Subdomains: ${config.subdomains ? (config.baseDomain || 'enabled') : 'disabled'}`);
|
|
334
|
-
console.log(` Mashlib: ${config.mashlibModule ? `module (${config.mashlibModule})` : config.mashlibCdn ? `CDN v${config.mashlibVersion}` :
|
|
335
|
-
console.log(` SolidOS UI: ${config.solidosUi ? 'enabled' : 'disabled'}`);
|
|
330
|
+
console.log(` Mashlib: ${config.mashlibModule ? `module (${config.mashlibModule})` : config.mashlibCdn ? `CDN v${config.mashlibVersion}` : 'disabled'}`);
|
|
336
331
|
if (config.pay) {
|
|
337
332
|
console.log(` Pay: ${config.payCost} sat/req`);
|
|
338
333
|
if (config.payToken) console.log(` Token: ${config.payToken} @ ${config.payRate} sat/token`);
|
package/src/handlers/resource.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from '../rdf/conneg.js';
|
|
16
16
|
import { emitChange } from '../notifications/events.js';
|
|
17
17
|
import { checkIfMatch, checkIfNoneMatchForGet, checkIfNoneMatchForWrite } from '../utils/conditional.js';
|
|
18
|
-
import { generateDatabrowserHtml, generateModuleDatabrowserHtml,
|
|
18
|
+
import { generateDatabrowserHtml, generateModuleDatabrowserHtml, shouldServeMashlib } from '../mashlib/index.js';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Live reload script - injected into HTML when --live-reload is enabled
|
|
@@ -230,12 +230,9 @@ export async function handleGet(request, reply) {
|
|
|
230
230
|
|
|
231
231
|
// Check if we should serve Mashlib data browser for containers
|
|
232
232
|
if (shouldServeMashlib(request, request.mashlibEnabled, 'application/ld+json')) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
?
|
|
236
|
-
: request.mashlibModule
|
|
237
|
-
? generateModuleDatabrowserHtml(request.mashlibModule)
|
|
238
|
-
: generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
|
|
233
|
+
const html = request.mashlibModule
|
|
234
|
+
? generateModuleDatabrowserHtml(request.mashlibModule)
|
|
235
|
+
: generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
|
|
239
236
|
const headers = getAllHeaders({
|
|
240
237
|
isContainer: true,
|
|
241
238
|
etag: stats.etag,
|
|
@@ -309,12 +306,9 @@ export async function handleGet(request, reply) {
|
|
|
309
306
|
// Check if we should serve Mashlib data browser
|
|
310
307
|
// Only for RDF resources when Accept: text/html is requested
|
|
311
308
|
if (shouldServeMashlib(request, request.mashlibEnabled, storedContentType)) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
?
|
|
315
|
-
: request.mashlibModule
|
|
316
|
-
? generateModuleDatabrowserHtml(request.mashlibModule)
|
|
317
|
-
: generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
|
|
309
|
+
const html = request.mashlibModule
|
|
310
|
+
? generateModuleDatabrowserHtml(request.mashlibModule)
|
|
311
|
+
: generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
|
|
318
312
|
const headers = getAllHeaders({
|
|
319
313
|
isContainer: false,
|
|
320
314
|
etag: stats.etag,
|
package/src/server.js
CHANGED
|
@@ -62,14 +62,11 @@ export function createServer(options = {}) {
|
|
|
62
62
|
const subdomainsEnabled = options.subdomains ?? false;
|
|
63
63
|
const baseDomain = options.baseDomain || null;
|
|
64
64
|
// Mashlib data browser is OFF by default
|
|
65
|
-
// mashlibCdn:
|
|
66
|
-
// mashlibModule: URL to ES module entry point (alternative to classic mashlib)
|
|
65
|
+
// mashlibCdn: load from CDN; mashlibModule: URL to ES module entry point
|
|
67
66
|
const mashlibModule = options.mashlibModule ?? false;
|
|
68
|
-
const mashlibEnabled = options.mashlib || !!mashlibModule;
|
|
69
67
|
const mashlibCdn = options.mashlibCdn ?? false;
|
|
68
|
+
const mashlibEnabled = mashlibCdn || !!mashlibModule;
|
|
70
69
|
const mashlibVersion = options.mashlibVersion ?? '2.0.0';
|
|
71
|
-
// SolidOS UI (modern Nextcloud-style interface) - requires mashlib
|
|
72
|
-
const solidosUiEnabled = options.solidosUi ?? false;
|
|
73
70
|
// Git HTTP backend is OFF by default - enables clone/push via git protocol
|
|
74
71
|
const gitEnabled = options.git ?? false;
|
|
75
72
|
// Nostr relay is OFF by default
|
|
@@ -179,7 +176,6 @@ export function createServer(options = {}) {
|
|
|
179
176
|
fastify.decorateRequest('mashlibCdn', null);
|
|
180
177
|
fastify.decorateRequest('mashlibVersion', null);
|
|
181
178
|
fastify.decorateRequest('mashlibModule', null);
|
|
182
|
-
fastify.decorateRequest('solidosUiEnabled', null);
|
|
183
179
|
fastify.decorateRequest('defaultQuota', null);
|
|
184
180
|
fastify.decorateRequest('config', null);
|
|
185
181
|
fastify.decorateRequest('liveReloadEnabled', null);
|
|
@@ -193,7 +189,6 @@ export function createServer(options = {}) {
|
|
|
193
189
|
request.mashlibCdn = mashlibCdn;
|
|
194
190
|
request.mashlibVersion = mashlibVersion;
|
|
195
191
|
request.mashlibModule = mashlibModule;
|
|
196
|
-
request.solidosUiEnabled = solidosUiEnabled;
|
|
197
192
|
request.defaultQuota = defaultQuota;
|
|
198
193
|
request.config = { public: options.public, readOnly: options.readOnly };
|
|
199
194
|
request.liveReloadEnabled = liveReloadEnabled;
|
|
@@ -410,7 +405,7 @@ export function createServer(options = {}) {
|
|
|
410
405
|
// Authorization hook - check WAC permissions
|
|
411
406
|
// Skip for pod creation endpoint (needs special handling)
|
|
412
407
|
fastify.addHook('preHandler', async (request, reply) => {
|
|
413
|
-
// Skip auth for pod creation, OPTIONS, IdP routes, mashlib,
|
|
408
|
+
// Skip auth for pod creation, OPTIONS, IdP routes, mashlib, well-known, notifications, nostr, git, and AP
|
|
414
409
|
const mashlibPaths = ['/mashlib.min.js', '/mash.css', '/841.mashlib.min.js'];
|
|
415
410
|
const apPaths = ['/inbox', '/profile/card/inbox', '/profile/card/outbox', '/profile/card/followers', '/profile/card/following',
|
|
416
411
|
'/api/v1/apps', '/api/v1/instance', '/api/v1/accounts/verify_credentials',
|
|
@@ -424,7 +419,6 @@ export function createServer(options = {}) {
|
|
|
424
419
|
request.method === 'OPTIONS' ||
|
|
425
420
|
request.url.startsWith('/idp/') ||
|
|
426
421
|
request.url.startsWith('/.well-known/') ||
|
|
427
|
-
request.url.startsWith('/solidos-ui/') ||
|
|
428
422
|
(nostrEnabled && request.url.startsWith(nostrPath)) ||
|
|
429
423
|
(gitEnabled && isGitRequest(request.url)) ||
|
|
430
424
|
(activitypubEnabled && apPaths.some(p => request.url === p || request.url.startsWith(p + '?'))) ||
|
|
@@ -464,72 +458,15 @@ export function createServer(options = {}) {
|
|
|
464
458
|
}
|
|
465
459
|
}, handleCreatePod);
|
|
466
460
|
|
|
467
|
-
// Mashlib
|
|
468
|
-
if (mashlibEnabled) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
// Mashlib uses code splitting, so it loads chunks like 789.mashlib.min.js
|
|
472
|
-
const cdnBase = `https://unpkg.com/mashlib@${mashlibVersion}/dist`;
|
|
473
|
-
const chunkPattern = /^\/\d+\.mashlib\.min\.js(\.map)?$/;
|
|
474
|
-
|
|
475
|
-
fastify.addHook('onRequest', async (request, reply) => {
|
|
476
|
-
if (chunkPattern.test(request.url)) {
|
|
477
|
-
const filename = request.url.split('/').pop();
|
|
478
|
-
return reply.redirect(302, `${cdnBase}/${filename}`);
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
} else {
|
|
482
|
-
// Local mode: serve from local files
|
|
483
|
-
const mashlibDir = join(__dirname, 'mashlib-local', 'dist');
|
|
484
|
-
const mashlibFiles = {
|
|
485
|
-
'/mashlib.min.js': { file: 'mashlib.min.js', type: 'application/javascript' },
|
|
486
|
-
'/mashlib.min.js.map': { file: 'mashlib.min.js.map', type: 'application/json' },
|
|
487
|
-
'/mash.css': { file: 'mash.css', type: 'text/css' },
|
|
488
|
-
'/mash.css.map': { file: 'mash.css.map', type: 'application/json' },
|
|
489
|
-
'/841.mashlib.min.js': { file: '841.mashlib.min.js', type: 'application/javascript' },
|
|
490
|
-
'/841.mashlib.min.js.map': { file: '841.mashlib.min.js.map', type: 'application/json' }
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
for (const [path, config] of Object.entries(mashlibFiles)) {
|
|
494
|
-
fastify.get(path, async (request, reply) => {
|
|
495
|
-
try {
|
|
496
|
-
const content = await readFile(join(mashlibDir, config.file));
|
|
497
|
-
return reply.type(config.type).send(content);
|
|
498
|
-
} catch {
|
|
499
|
-
return reply.code(404).send({ error: 'Not Found' });
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
461
|
+
// Mashlib CDN mode: redirect chunk requests to CDN
|
|
462
|
+
if (mashlibEnabled && mashlibCdn) {
|
|
463
|
+
const cdnBase = `https://unpkg.com/mashlib@${mashlibVersion}/dist`;
|
|
464
|
+
const chunkPattern = /^\/\d+\.mashlib\.min\.js(\.map)?$/;
|
|
505
465
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
// Serve all files under /solidos-ui/* path
|
|
512
|
-
fastify.get('/solidos-ui/*', async (request, reply) => {
|
|
513
|
-
try {
|
|
514
|
-
// Get the path after /solidos-ui/
|
|
515
|
-
const filePath = request.url.replace('/solidos-ui/', '').split('?')[0];
|
|
516
|
-
const fullPath = join(solidosUiDir, filePath);
|
|
517
|
-
|
|
518
|
-
// Determine content type based on extension
|
|
519
|
-
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
520
|
-
const contentTypes = {
|
|
521
|
-
'js': 'application/javascript',
|
|
522
|
-
'css': 'text/css',
|
|
523
|
-
'map': 'application/json',
|
|
524
|
-
'html': 'text/html'
|
|
525
|
-
};
|
|
526
|
-
const contentType = contentTypes[ext] || 'application/octet-stream';
|
|
527
|
-
|
|
528
|
-
const content = await readFile(fullPath);
|
|
529
|
-
return reply.type(contentType).send(content);
|
|
530
|
-
} catch (err) {
|
|
531
|
-
request.log.error(err, 'Failed to serve solidos-ui file');
|
|
532
|
-
return reply.code(404).send({ error: 'Not Found' });
|
|
466
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
467
|
+
if (chunkPattern.test(request.url)) {
|
|
468
|
+
const filename = request.url.split('/').pop();
|
|
469
|
+
return reply.redirect(302, `${cdnBase}/${filename}`);
|
|
533
470
|
}
|
|
534
471
|
});
|
|
535
472
|
}
|