javascript-solid-server 0.0.86 → 0.0.88

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.
@@ -324,7 +324,8 @@
324
324
  "Bash(npm link)",
325
325
  "Bash(npm link:*)",
326
326
  "Bash(git push)",
327
- "Bash(ulimit:*)"
327
+ "Bash(ulimit:*)",
328
+ "Bash(gh label:*)"
328
329
  ]
329
330
  }
330
331
  }
package/README.md CHANGED
@@ -1,13 +1,18 @@
1
1
  # JavaScript Solid Server
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/javascript-solid-server)](https://www.npmjs.com/package/javascript-solid-server)
4
+
3
5
  A minimal, fast, JSON-LD native Solid server.
4
6
 
5
7
  **[Documentation](https://javascriptsolidserver.github.io/docs/)** | **[GitHub](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer)**
6
8
 
7
9
  ## Features
8
10
 
9
- ### Implemented (v0.0.79)
11
+ ### Implemented
10
12
 
13
+ - **Live Reload** - Auto-refresh browser on file changes (`--live-reload`)
14
+ - **Read-Only Mode** - Disable write operations for static hosting (`--read-only`)
15
+ - **Public Mode** - Skip WAC for open read/write access (`--public`)
11
16
  - **Schnorr SSO** - Passwordless login via BIP-340 Schnorr signatures using NIP-07 browser extensions (Podkey, nos2x, Alby)
12
17
  - **Passkey Authentication** - WebAuthn/FIDO2 passwordless login with Touch ID, Face ID, or security keys
13
18
  - **HTTP Range Requests** - Partial content delivery for large files and media streaming
@@ -35,7 +40,7 @@ A minimal, fast, JSON-LD native Solid server.
35
40
  - **Content Negotiation** - Turtle <-> JSON-LD conversion, including HTML data islands
36
41
  - **CORS Support** - Full cross-origin resource sharing
37
42
  - **Git HTTP Backend** - Clone and push to containers via `git` protocol
38
- - **Nostr Relay** - Integrated NIP-01 relay on the same port (`wss://your.pod/relay`)
43
+ - **Nostr Relay** - Integrated NIP-01/NIP-11/NIP-16 relay on the same port (`wss://your.pod/relay`)
39
44
  - **Invite-Only Registration** - CLI-managed invite codes for controlled signups
40
45
  - **Storage Quotas** - Per-user storage limits with CLI management
41
46
  - **Security** - Blocks access to dotfiles (`.git/`, `.env`, etc.) except Solid-specific ones
@@ -139,6 +144,9 @@ jss --help # Show help
139
144
  | `--ap-display-name <name>` | ActivityPub display name | (username) |
140
145
  | `--ap-summary <text>` | ActivityPub bio/summary | - |
141
146
  | `--ap-nostr-pubkey <hex>` | Nostr pubkey for identity linking | - |
147
+ | `--public` | Allow unauthenticated access (skip WAC) | false |
148
+ | `--read-only` | Disable PUT/DELETE/PATCH methods | false |
149
+ | `--live-reload` | Auto-refresh browser on file changes | false |
142
150
  | `-q, --quiet` | Suppress logs | false |
143
151
 
144
152
  ### Environment Variables
@@ -159,6 +167,10 @@ export JSS_WEBID_TLS=true
159
167
  export JSS_DEFAULT_QUOTA=100MB
160
168
  export JSS_ACTIVITYPUB=true
161
169
  export JSS_AP_USERNAME=alice
170
+ export JSS_PUBLIC=true
171
+ export JSS_READ_ONLY=true
172
+ export JSS_LIVE_RELOAD=true
173
+ export JSS_SOLIDOS_UI=true
162
174
  jss start
163
175
  ```
164
176
 
@@ -868,7 +880,7 @@ curl -X POST https://example.com/.pods \
868
880
 
869
881
  | Server | Size | Deps | Notes |
870
882
  |--------|------|------|-------|
871
- | [JSS](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer) | 432 KB | 10 | Minimal, JSON-LD native |
883
+ | [JSS](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer) | ~14K LoC | 14 | Minimal, JSON-LD native |
872
884
  | [NSS](https://github.com/nodeSolidServer/node-solid-server) | 777 KB | 58 | Original Solid server |
873
885
  | [CSS](https://github.com/CommunitySolidServer/CommunitySolidServer) | 5.8 MB | 70 | Modular, configurable |
874
886
  | [Pivot](https://github.com/solid-contrib/pivot) | ~6 MB | 70+ | Built on CSS |
@@ -941,7 +953,7 @@ npm run benchmark
941
953
  npm test
942
954
  ```
943
955
 
944
- Currently passing: **223 tests** (including 27 conformance tests)
956
+ Currently passing: **229 tests** (including 27 conformance tests)
945
957
 
946
958
  ### Conformance Test Harness (CTH)
947
959
 
@@ -988,12 +1000,12 @@ src/
988
1000
  │ ├── filesystem.js # File operations
989
1001
  │ └── quota.js # Storage quota management
990
1002
  ├── auth/
991
- │ ├── middleware.js # Auth hook
992
- │ ├── token.js # Simple token auth
993
- │ ├── solid-oidc.js # DPoP verification
994
- │ ├── nostr.js # NIP-98 Nostr authentication
995
- │ ├── did-nostr.js # did:nostr → WebID resolution
996
- │ └── webid-tls.js # WebID-TLS client certificate auth
1003
+ │ ├── middleware.js # Auth hook
1004
+ │ ├── token.js # Simple token auth
1005
+ │ ├── solid-oidc.js # DPoP verification
1006
+ │ ├── nostr.js # NIP-98 Nostr authentication
1007
+ │ ├── did-nostr.js # did:nostr → WebID resolution
1008
+ │ └── webid-tls.js # WebID-TLS client certificate auth
997
1009
  ├── wac/
998
1010
  │ ├── parser.js # ACL parsing
999
1011
  │ └── checker.js # Permission checking
@@ -1010,14 +1022,16 @@ src/
1010
1022
  │ ├── events.js # Event emitter
1011
1023
  │ └── websocket.js # solid-0.1 protocol
1012
1024
  ├── idp/
1013
- │ ├── index.js # Identity Provider plugin
1014
- │ ├── provider.js # oidc-provider config
1015
- │ ├── adapter.js # Filesystem adapter
1016
- │ ├── accounts.js # User account management
1017
- │ ├── keys.js # JWKS key management
1018
- │ ├── interactions.js # Login/consent handlers
1019
- │ ├── views.js # HTML templates
1020
- └── invites.js # Invite code management
1025
+ │ ├── index.js # Identity Provider plugin
1026
+ │ ├── provider.js # oidc-provider config
1027
+ │ ├── adapter.js # Filesystem adapter
1028
+ │ ├── accounts.js # User account management
1029
+ │ ├── credentials.js # Credentials endpoint
1030
+ │ ├── keys.js # JWKS key management
1031
+ │ ├── interactions.js # Login/consent handlers
1032
+ ├── passkey.js # WebAuthn/FIDO2 passkey support
1033
+ │ ├── views.js # HTML templates
1034
+ │ └── invites.js # Invite code management
1021
1035
  ├── ap/
1022
1036
  │ ├── index.js # ActivityPub plugin
1023
1037
  │ ├── keys.js # RSA keypair management
@@ -1030,23 +1044,31 @@ src/
1030
1044
  ├── rdf/
1031
1045
  │ ├── turtle.js # Turtle <-> JSON-LD
1032
1046
  │ └── conneg.js # Content negotiation
1047
+ ├── mashlib/
1048
+ │ └── index.js # Mashlib data browser plugin
1033
1049
  └── utils/
1034
- ├── url.js # URL utilities
1035
- └── conditional.js # If-Match/If-None-Match
1050
+ ├── url.js # URL utilities
1051
+ ├── conditional.js # If-Match/If-None-Match
1052
+ └── ssrf.js # SSRF protection
1036
1053
  ```
1037
1054
 
1038
1055
  ## Dependencies
1039
1056
 
1040
- Minimal dependencies for a fast, secure server:
1057
+ 14 direct dependencies for a fast, secure server:
1041
1058
 
1042
1059
  - **fastify** - High-performance HTTP server
1060
+ - **@fastify/middie** - Express/Connect middleware bridge (for IdP)
1061
+ - **@fastify/rate-limit** - Rate limiting for API endpoints
1043
1062
  - **@fastify/websocket** - WebSocket support for notifications
1063
+ - **@simplewebauthn/server** - Passkey/WebAuthn authentication
1064
+ - **bcryptjs** - Password hashing (pure JS, works on Termux/Android)
1065
+ - **commander** - CLI command parsing
1044
1066
  - **fs-extra** - Enhanced file operations
1045
1067
  - **jose** - JWT/JWK handling for Solid-OIDC
1068
+ - **microfed** - ActivityPub primitives (only when activitypub enabled)
1046
1069
  - **n3** - Turtle parsing (only used when conneg enabled)
1070
+ - **nostr-tools** - Nostr protocol and Schnorr signature verification
1047
1071
  - **oidc-provider** - OpenID Connect Identity Provider (only when IdP enabled)
1048
- - **bcryptjs** - Password hashing (only when IdP enabled)
1049
- - **microfed** - ActivityPub primitives (only when activitypub enabled)
1050
1072
  - **sql.js** - SQLite storage for federation data (WASM, cross-platform)
1051
1073
 
1052
1074
  ## License
package/bin/jss.js CHANGED
@@ -80,6 +80,7 @@ program
80
80
  .option('--read-only', 'Disable PUT/DELETE/PATCH methods (read-only mode)')
81
81
  .option('--live-reload', 'Inject live reload script into HTML (auto-refresh on changes)')
82
82
  .option('-q, --quiet', 'Suppress log output')
83
+ .option('--log-level <level>', 'Log level: error, warn, info, debug (default: info)')
83
84
  .option('--print-config', 'Print configuration and exit')
84
85
  .action(async (options) => {
85
86
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.86",
3
+ "version": "0.0.88",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -13,6 +13,28 @@ const DEFAULT_DID_RESOLVER = 'https://nostr.social/.well-known/did/nostr';
13
13
  // Cache for resolved DIDs (pubkey -> webId or null)
14
14
  const cache = new Map();
15
15
  const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
16
+ const FAILURE_CACHE_TTL = 60 * 1000; // 1 minute for failed lookups
17
+
18
+ // Rate-limit repeated error logs (key -> { count, lastLogged })
19
+ const errorLogTracker = new Map();
20
+ const ERROR_LOG_INTERVAL = 60_000;
21
+
22
+ function rateLimitedError(key, message) {
23
+ const now = Date.now();
24
+ const entry = errorLogTracker.get(key);
25
+ if (entry && now - entry.lastLogged < ERROR_LOG_INTERVAL) {
26
+ entry.count++;
27
+ return;
28
+ }
29
+ // Clean up stale entries while we're here
30
+ for (const [k, v] of errorLogTracker) {
31
+ if (now - v.lastLogged > ERROR_LOG_INTERVAL) errorLogTracker.delete(k);
32
+ }
33
+ const suppressed = entry ? entry.count : 0;
34
+ const suffix = suppressed > 0 ? ` (${suppressed} similar suppressed)` : '';
35
+ console.error(`${message}${suffix}`);
36
+ errorLogTracker.set(key, { count: 0, lastLogged: now });
37
+ }
16
38
 
17
39
  /**
18
40
  * Fetch with timeout
@@ -41,11 +63,15 @@ export async function resolveDidNostrToWebId(pubkey, resolverUrl = DEFAULT_DID_R
41
63
  return null;
42
64
  }
43
65
 
44
- // Check cache
66
+ // Check cache (lazy eviction of expired entries)
45
67
  const cacheKey = pubkey.toLowerCase();
46
68
  const cached = cache.get(cacheKey);
47
- if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
48
- return cached.webId;
69
+ if (cached) {
70
+ const ttl = cached.failureTtl ? FAILURE_CACHE_TTL : CACHE_TTL;
71
+ if (Date.now() - cached.timestamp < ttl) {
72
+ return cached.webId;
73
+ }
74
+ cache.delete(cacheKey);
49
75
  }
50
76
 
51
77
  try {
@@ -93,8 +119,9 @@ export async function resolveDidNostrToWebId(pubkey, resolverUrl = DEFAULT_DID_R
93
119
  return null;
94
120
 
95
121
  } catch (err) {
96
- // Network error or timeout - don't cache failures
97
- console.error(`DID resolution error for ${pubkey}:`, err.message);
122
+ // Cache failures with short TTL to avoid hammering a down service
123
+ cache.set(cacheKey, { webId: null, timestamp: Date.now(), failureTtl: true });
124
+ rateLimitedError(`did:${pubkey.substring(0, 8)}`, `DID resolution error for ${pubkey}: ${err.message}`);
98
125
  return null;
99
126
  }
100
127
  }
@@ -148,7 +175,7 @@ async function verifyWebIdBacklink(webId, pubkey) {
148
175
  return false;
149
176
 
150
177
  } catch (err) {
151
- console.error(`WebID backlink verification error for ${webId}:`, err.message);
178
+ rateLimitedError(`backlink:${webId}`, `WebID backlink verification error for ${webId}: ${err.message}`);
152
179
  return false;
153
180
  }
154
181
  }
package/src/config.js CHANGED
@@ -85,6 +85,7 @@ export const defaults = {
85
85
  // Logging
86
86
  logger: true,
87
87
  quiet: false,
88
+ logLevel: 'info',
88
89
 
89
90
  // Paths
90
91
  configPath: './.jss',
@@ -103,6 +104,7 @@ const envMap = {
103
104
  JSS_CONNEG: 'conneg',
104
105
  JSS_NOTIFICATIONS: 'notifications',
105
106
  JSS_QUIET: 'quiet',
107
+ JSS_LOG_LEVEL: 'logLevel',
106
108
  JSS_CONFIG_PATH: 'configPath',
107
109
  JSS_IDP: 'idp',
108
110
  JSS_IDP_ISSUER: 'idpIssuer',
@@ -228,6 +230,18 @@ export async function loadConfig(cliOptions = {}, configFile = null) {
228
230
  config.logger = false;
229
231
  }
230
232
 
233
+ // Validate log level
234
+ const validLevels = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'];
235
+ if (!validLevels.includes(config.logLevel)) {
236
+ console.warn(`Invalid log level '${config.logLevel}', falling back to 'info'. Valid levels: ${validLevels.join(', ')}`);
237
+ config.logLevel = 'info';
238
+ }
239
+
240
+ // Mashlib requires content negotiation for Turtle support
241
+ if (config.mashlib || config.mashlibCdn) {
242
+ config.conneg = true;
243
+ }
244
+
231
245
  // Validate SSL config
232
246
  if ((config.sslKey && !config.sslCert) || (!config.sslKey && config.sslCert)) {
233
247
  throw new Error('Both --ssl-key and --ssl-cert must be provided together');
package/src/server.js CHANGED
@@ -89,8 +89,10 @@ export function createServer(options = {}) {
89
89
  }
90
90
 
91
91
  // Fastify options
92
+ const loggerEnabled = options.logger ?? true;
92
93
  const fastifyOptions = {
93
- logger: options.logger ?? true,
94
+ logger: loggerEnabled ? { level: options.logLevel || 'info' } : false,
95
+ disableRequestLogging: true,
94
96
  trustProxy: true,
95
97
  // Handle raw body for non-JSON content
96
98
  bodyLimit: 10 * 1024 * 1024 // 10MB
@@ -169,6 +171,19 @@ export function createServer(options = {}) {
169
171
  }
170
172
  });
171
173
 
174
+ // Unified access log — one line per request
175
+ fastify.addHook('onResponse', async (request, reply) => {
176
+ if (!request.log.isLevelEnabled('info')) return;
177
+ request.log.info({
178
+ req: { method: request.method, url: request.url, remoteAddress: request.ip },
179
+ res: { statusCode: reply.statusCode },
180
+ responseTime: Math.round(reply.elapsedTime * 100) / 100,
181
+ userAgent: request.headers['user-agent'] || undefined,
182
+ referrer: request.headers.referer || undefined,
183
+ contentLength: reply.getHeader('content-length') || undefined,
184
+ }, `${request.method} ${request.url} ${reply.statusCode} ${Math.round(reply.elapsedTime)}ms`);
185
+ });
186
+
172
187
  // Register WebSocket notifications plugin if enabled (or live reload needs it)
173
188
  if (notificationsEnabled || liveReloadEnabled) {
174
189
  fastify.register(notificationsPlugin);
package/src/wac/parser.js CHANGED
@@ -144,8 +144,9 @@ function parseAuthorization(node, aclUrl) {
144
144
  auth.default = parseUriArray(node['acl:default'] || node['default'])
145
145
  .map(uri => resolveUri(uri, baseUrl));
146
146
 
147
- // Parse agents (WebIDs can be relative too)
148
- auth.agents = parseUriArray(node['acl:agent'] || node['agent']);
147
+ // Parse agents (WebIDs can be relative too) - resolve against ACL URL
148
+ auth.agents = parseUriArray(node['acl:agent'] || node['agent'])
149
+ .map(uri => resolveUri(uri, aclUrl));
149
150
 
150
151
  // Parse agentClass
151
152
  auth.agentClasses = parseUriArray(node['acl:agentClass'] || node['agentClass']);
package/test/wac.test.js CHANGED
@@ -187,6 +187,23 @@ describe('WAC Parser', () => {
187
187
  `Expected accessTo to include 'https://alice.example/other/', got: ${auths[0].accessTo}`);
188
188
  });
189
189
 
190
+ it('should resolve relative agent URIs against ACL URL', async () => {
191
+ const acl = {
192
+ '@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' },
193
+ '@id': '#owner',
194
+ '@type': 'acl:Authorization',
195
+ 'acl:agent': { '@id': './#me' },
196
+ 'acl:accessTo': { '@id': './' },
197
+ 'acl:mode': [{ '@id': 'acl:Read' }]
198
+ };
199
+
200
+ const auths = await parseAcl(JSON.stringify(acl), 'https://alice.example/.acl');
201
+
202
+ assert.strictEqual(auths.length, 1);
203
+ assert.ok(auths[0].agents.includes('https://alice.example/#me'),
204
+ `Expected agents to include 'https://alice.example/#me', got: ${auths[0].agents}`);
205
+ });
206
+
190
207
  it('should keep absolute URLs unchanged', async () => {
191
208
  const acl = {
192
209
  '@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' },
@@ -1,120 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>fonstr - Your Nostr Relay</title>
7
- <style>
8
- * { margin: 0; padding: 0; box-sizing: border-box; }
9
- body {
10
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
11
- background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
12
- color: white;
13
- min-height: 100vh;
14
- display: flex;
15
- align-items: center;
16
- justify-content: center;
17
- padding: 2rem;
18
- }
19
- .container {
20
- text-align: center;
21
- max-width: 600px;
22
- }
23
- h1 {
24
- font-size: 3rem;
25
- margin-bottom: 0.5rem;
26
- }
27
- .emoji {
28
- font-size: 4rem;
29
- margin-bottom: 1rem;
30
- }
31
- p {
32
- font-size: 1.25rem;
33
- opacity: 0.95;
34
- margin-bottom: 2rem;
35
- line-height: 1.6;
36
- }
37
- .relay-info {
38
- background: rgba(255, 255, 255, 0.2);
39
- backdrop-filter: blur(10px);
40
- border-radius: 1rem;
41
- padding: 2rem;
42
- margin: 2rem 0;
43
- }
44
- code {
45
- background: rgba(255, 255, 255, 0.3);
46
- padding: 0.5rem 1rem;
47
- border-radius: 0.5rem;
48
- font-size: 1.1rem;
49
- display: inline-block;
50
- margin: 0.5rem 0;
51
- font-family: 'Monaco', 'Menlo', monospace;
52
- }
53
- .stats {
54
- display: grid;
55
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
56
- gap: 1rem;
57
- margin-top: 2rem;
58
- }
59
- .stat {
60
- background: rgba(255, 255, 255, 0.15);
61
- padding: 1rem;
62
- border-radius: 0.5rem;
63
- }
64
- .stat-label {
65
- font-size: 0.9rem;
66
- opacity: 0.8;
67
- }
68
- .stat-value {
69
- font-size: 1.5rem;
70
- font-weight: 700;
71
- margin-top: 0.25rem;
72
- }
73
- a {
74
- color: white;
75
- text-decoration: none;
76
- border-bottom: 2px solid rgba(255, 255, 255, 0.5);
77
- transition: border-color 0.2s;
78
- }
79
- a:hover {
80
- border-color: white;
81
- }
82
- </style>
83
- </head>
84
- <body>
85
- <div class="container">
86
- <div class="emoji">⚡</div>
87
- <h1>fonstr</h1>
88
- <p>Your Nostr relay is running!</p>
89
-
90
- <div class="relay-info">
91
- <p style="font-size: 1rem; margin-bottom: 1rem; opacity: 0.9;">Connect to your relay:</p>
92
- <code>ws://localhost:4444/relay</code>
93
-
94
- <div class="stats">
95
- <div class="stat">
96
- <div class="stat-label">Status</div>
97
- <div class="stat-value">✓ Online</div>
98
- </div>
99
- <div class="stat">
100
- <div class="stat-label">Protocol</div>
101
- <div class="stat-value">NIP-01</div>
102
- </div>
103
- <div class="stat">
104
- <div class="stat-label">Port</div>
105
- <div class="stat-value">4444</div>
106
- </div>
107
- </div>
108
- </div>
109
-
110
- <p style="font-size: 1rem;">
111
- Add this relay to your favorite Nostr client and start using it!<br>
112
- <a href="https://fonstr.com" target="_blank">Learn more about fonstr</a>
113
- </p>
114
-
115
- <p style="font-size: 0.9rem; opacity: 0.7; margin-top: 2rem;">
116
- Replace this page by editing <code style="font-size: 0.8rem;">index.html</code> in your data directory
117
- </p>
118
- </div>
119
- </body>
120
- </html>