Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 107 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ A minimal, fast, JSON-LD native Solid server.
- **HTTP Range Requests** - Partial content delivery for large files and media streaming
- **Single-User Mode** - Simplified setup for personal pod servers
- **ActivityPub Federation** - Fediverse integration with WebFinger, inbox/outbox, HTTP signatures
- **Mastodon-compatible API** - Dynamic client registration, instance info, account verification
- **OAuth 2.0 Authorization** - Shared auth flow for Mastodon clients, remoteStorage apps, and third-party panes
- **remoteStorage Protocol** - [draft-dejong-remotestorage-22](https://remotestorage.io/spec/) file sync (requires `--activitypub` for WebFinger discovery + OAuth)
- **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
- **N3 Patch** - Solid's native patch format for RDF updates
- **SPARQL Update** - Standard SPARQL UPDATE protocol for PATCH
Expand Down Expand Up @@ -554,6 +557,106 @@ curl -H "Accept: application/activity+json" http://localhost:3000/profile/card
curl http://localhost:3000/.well-known/nodeinfo/2.1
```

## Mastodon-compatible API

JSS exposes Mastodon API endpoints so that Mastodon clients (Elk, Phanpy, Ice Cubes) can connect:

```bash
jss start --activitypub --idp
```

### Endpoints

| Endpoint | Description |
|----------|-------------|
| `POST /api/v1/apps` | Dynamic client registration |
| `GET /api/v1/accounts/verify_credentials` | Current user profile |
| `GET /api/v1/instance` | Instance metadata |
| `GET /oauth/authorize` | OAuth authorize page |
| `POST /oauth/authorize` | Process login |
| `POST /oauth/token` | Exchange code for Bearer token |

### OAuth 2.0 Flow

The OAuth layer is shared between Mastodon clients, remoteStorage apps, and third-party Solid panes:

1. Client registers via `POST /api/v1/apps` (gets `client_id` + `client_secret`)
2. Client redirects user to `GET /oauth/authorize?client_id=...&redirect_uri=...&response_type=code`
3. User logs in, JSS redirects back with `?code=...`
4. Client exchanges code for Bearer token via `POST /oauth/token`
5. Bearer token works with all JSS endpoints (Solid, ActivityPub, remoteStorage)

Supports out-of-band (OOB) redirect for CLI/desktop clients.

### Testing

```bash
# Register a client
curl -X POST http://localhost:3000/api/v1/apps \
-H "Content-Type: application/json" \
-d '{"client_name": "Test App", "redirect_uris": "urn:ietf:wg:oauth:2.0:oob"}'

# Check instance info
curl http://localhost:3000/api/v1/instance
```

## remoteStorage

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.

```bash
jss start --activitypub --idp
```

### Discovery

remoteStorage clients discover the storage endpoint via WebFinger:

```bash
curl "http://localhost:3000/.well-known/webfinger?resource=acct:me@localhost:3000"
```

The response includes a `remotestorage` link relation pointing to `/storage/me/`.

### Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/storage/:user/*` | Read file or list folder (JSON-LD) |
| `HEAD` | `/storage/:user/*` | Get metadata (ETag, Content-Type, size) |
| `PUT` | `/storage/:user/*` | Write file (creates parent folders) |
| `DELETE` | `/storage/:user/*` | Delete file |

### How It Works

- **Auth**: Bearer token via OAuth 2.0 (same flow as Mastodon clients)
- **Public folder**: `/storage/me/public/*` is readable without auth
- **Conditional requests**: If-Match, If-None-Match (uses shared ETag utilities)
- **Dotfile protection**: `.acl`, `.meta`, and other dotfiles are blocked
- **Read-only mode**: Respects `--read-only` flag
- **Streaming**: Large files are streamed, not buffered

### Testing

```bash
# Write a file (needs Bearer token from OAuth flow)
curl -X PUT http://localhost:3000/storage/me/documents/hello.txt \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: text/plain" \
-d "Hello, remoteStorage!"

# Read it back
curl -H "Authorization: Bearer YOUR_TOKEN" \
http://localhost:3000/storage/me/documents/hello.txt

# List a folder
curl -H "Authorization: Bearer YOUR_TOKEN" \
http://localhost:3000/storage/me/documents/

# Read from public folder (no auth needed)
curl http://localhost:3000/storage/me/public/readme.txt
```

### Linking Nostr to WebID (did:nostr)

Bridge your Nostr identity to a Solid WebID for seamless authentication:
Expand Down Expand Up @@ -1097,7 +1200,10 @@ src/
│ ├── actor.js # Actor JSON-LD
│ ├── inbox.js # Receive activities
│ ├── outbox.js # User's activities
│ └── collections.js # Followers/following
│ ├── collections.js # Followers/following
│ ├── mastodon.js # Mastodon API (apps, instance, verify_credentials)
│ └── oauth.js # OAuth 2.0 authorize/token flow
├── remotestorage.js # remoteStorage protocol (draft-dejong-remotestorage-22)
├── rdf/
│ ├── turtle.js # Turtle <-> JSON-LD
│ └── conneg.js # Content negotiation
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "javascript-solid-server",
"version": "0.0.96",
"version": "0.0.97",
"description": "A minimal, fast Solid server",
"main": "src/index.js",
"type": "module",
Expand Down
13 changes: 12 additions & 1 deletion src/ap/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ export async function activityPubPlugin(fastify, options = {}) {
{ profileUrl }
)

// Add remoteStorage link relation
response.links.push({
rel: 'http://tools.ietf.org/id/draft-dejong-remotestorage',
href: `${baseUrl}/storage/${config.username}/`,
properties: {
Comment on lines +111 to +115
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WebFinger response advertises storage at /storage/${config.username} regardless of the acct: username requested. This prevents multi-user discovery and can mislead clients if the requested acct doesn't match config.username. Consider using the parsed WebFinger username in the storage href (and ensuring the storage routes validate/authorize that user appropriately).

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +115
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The advertised remoteStorage href does not include a trailing slash, while storage routes are defined as /storage/:user/* (which may not match /storage/:user without /). To avoid clients hitting a 404 when dereferencing the advertised URL, either advertise /storage/:user/ or add a handler/redirect for /storage/:user.

Copilot uses AI. Check for mistakes.
'http://remotestorage.io/spec/version': 'draft-dejong-remotestorage-22',
'http://tools.ietf.org/html/rfc6749#section-4.2': `${baseUrl}/oauth/authorize`,
'http://tools.ietf.org/html/rfc6750#section-2.3': 'Bearer'
}
})
Comment on lines +111 to +120
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remoteStorage discovery link is added inside activityPubPlugin’s WebFinger handler. Since activitypub is disabled by default (and /oauth/* is also registered only in this plugin), remoteStorage clients won’t be able to discover the storage endpoint or complete the OAuth flow unless ActivityPub is enabled—contradicting the PR/README claim that remoteStorage is “always on, no flag needed”. Consider moving the WebFinger + OAuth pieces to an always-on plugin, or update the docs/description to reflect the --activitypub requirement.

Copilot uses AI. Check for mistakes.

return reply
.header('Content-Type', 'application/jrd+json')
.header('Access-Control-Allow-Origin', '*')
Expand Down Expand Up @@ -137,7 +148,7 @@ export async function activityPubPlugin(fastify, options = {}) {
version: '2.1',
software: {
name: 'jss',
version: '0.0.67',
version: '0.0.97',
repository: 'https://github.com/JavaScriptSolidServer/JavaScriptSolidServer'
},
protocols: ['activitypub', 'solid'],
Expand Down
2 changes: 1 addition & 1 deletion src/ap/routes/mastodon.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function createInstanceHandler (config) {
title: config.displayName || 'JSS',
description: 'SAND Stack: Solid + ActivityPub + Nostr + DID',
short_description: 'Solid pod with Mastodon-compatible API',
version: '4.0.0 (compatible; JSS 0.0.67)',
version: '4.0.0 (compatible; JSS 0.0.97)',
urls: {
streaming_api: `${wsProtocol}://${host}`
},
Expand Down
Loading