javascript-solid-server 0.0.85 → 0.0.87

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.
@@ -323,7 +323,9 @@
323
323
  "Bash(npm exec serve:*)",
324
324
  "Bash(npm link)",
325
325
  "Bash(npm link:*)",
326
- "Bash(git push)"
326
+ "Bash(git push)",
327
+ "Bash(ulimit:*)",
328
+ "Bash(gh label:*)"
327
329
  ]
328
330
  }
329
331
  }
package/README.md CHANGED
@@ -6,8 +6,11 @@ A minimal, fast, JSON-LD native Solid server.
6
6
 
7
7
  ## Features
8
8
 
9
- ### Implemented (v0.0.79)
9
+ ### Implemented (v0.0.86)
10
10
 
11
+ - **Live Reload** - Auto-refresh browser on file changes (`--live-reload`)
12
+ - **Read-Only Mode** - Disable write operations for static hosting (`--read-only`)
13
+ - **Public Mode** - Skip WAC for open read/write access (`--public`)
11
14
  - **Schnorr SSO** - Passwordless login via BIP-340 Schnorr signatures using NIP-07 browser extensions (Podkey, nos2x, Alby)
12
15
  - **Passkey Authentication** - WebAuthn/FIDO2 passwordless login with Touch ID, Face ID, or security keys
13
16
  - **HTTP Range Requests** - Partial content delivery for large files and media streaming
@@ -35,7 +38,7 @@ A minimal, fast, JSON-LD native Solid server.
35
38
  - **Content Negotiation** - Turtle <-> JSON-LD conversion, including HTML data islands
36
39
  - **CORS Support** - Full cross-origin resource sharing
37
40
  - **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`)
41
+ - **Nostr Relay** - Integrated NIP-01/NIP-11/NIP-16 relay on the same port (`wss://your.pod/relay`)
39
42
  - **Invite-Only Registration** - CLI-managed invite codes for controlled signups
40
43
  - **Storage Quotas** - Per-user storage limits with CLI management
41
44
  - **Security** - Blocks access to dotfiles (`.git/`, `.env`, etc.) except Solid-specific ones
@@ -139,6 +142,9 @@ jss --help # Show help
139
142
  | `--ap-display-name <name>` | ActivityPub display name | (username) |
140
143
  | `--ap-summary <text>` | ActivityPub bio/summary | - |
141
144
  | `--ap-nostr-pubkey <hex>` | Nostr pubkey for identity linking | - |
145
+ | `--public` | Allow unauthenticated access (skip WAC) | false |
146
+ | `--read-only` | Disable PUT/DELETE/PATCH methods | false |
147
+ | `--live-reload` | Auto-refresh browser on file changes | false |
142
148
  | `-q, --quiet` | Suppress logs | false |
143
149
 
144
150
  ### Environment Variables
@@ -159,6 +165,10 @@ export JSS_WEBID_TLS=true
159
165
  export JSS_DEFAULT_QUOTA=100MB
160
166
  export JSS_ACTIVITYPUB=true
161
167
  export JSS_AP_USERNAME=alice
168
+ export JSS_PUBLIC=true
169
+ export JSS_READ_ONLY=true
170
+ export JSS_LIVE_RELOAD=true
171
+ export JSS_SOLIDOS_UI=true
162
172
  jss start
163
173
  ```
164
174
 
@@ -868,7 +878,7 @@ curl -X POST https://example.com/.pods \
868
878
 
869
879
  | Server | Size | Deps | Notes |
870
880
  |--------|------|------|-------|
871
- | [JSS](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer) | 432 KB | 10 | Minimal, JSON-LD native |
881
+ | [JSS](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer) | ~14K LoC | 14 | Minimal, JSON-LD native |
872
882
  | [NSS](https://github.com/nodeSolidServer/node-solid-server) | 777 KB | 58 | Original Solid server |
873
883
  | [CSS](https://github.com/CommunitySolidServer/CommunitySolidServer) | 5.8 MB | 70 | Modular, configurable |
874
884
  | [Pivot](https://github.com/solid-contrib/pivot) | ~6 MB | 70+ | Built on CSS |
@@ -941,7 +951,7 @@ npm run benchmark
941
951
  npm test
942
952
  ```
943
953
 
944
- Currently passing: **223 tests** (including 27 conformance tests)
954
+ Currently passing: **229 tests** (including 27 conformance tests)
945
955
 
946
956
  ### Conformance Test Harness (CTH)
947
957
 
@@ -988,12 +998,12 @@ src/
988
998
  │ ├── filesystem.js # File operations
989
999
  │ └── quota.js # Storage quota management
990
1000
  ├── 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
1001
+ │ ├── middleware.js # Auth hook
1002
+ │ ├── token.js # Simple token auth
1003
+ │ ├── solid-oidc.js # DPoP verification
1004
+ │ ├── nostr.js # NIP-98 Nostr authentication
1005
+ │ ├── did-nostr.js # did:nostr → WebID resolution
1006
+ │ └── webid-tls.js # WebID-TLS client certificate auth
997
1007
  ├── wac/
998
1008
  │ ├── parser.js # ACL parsing
999
1009
  │ └── checker.js # Permission checking
@@ -1010,14 +1020,16 @@ src/
1010
1020
  │ ├── events.js # Event emitter
1011
1021
  │ └── websocket.js # solid-0.1 protocol
1012
1022
  ├── 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
1023
+ │ ├── index.js # Identity Provider plugin
1024
+ │ ├── provider.js # oidc-provider config
1025
+ │ ├── adapter.js # Filesystem adapter
1026
+ │ ├── accounts.js # User account management
1027
+ │ ├── credentials.js # Credentials endpoint
1028
+ │ ├── keys.js # JWKS key management
1029
+ │ ├── interactions.js # Login/consent handlers
1030
+ ├── passkey.js # WebAuthn/FIDO2 passkey support
1031
+ │ ├── views.js # HTML templates
1032
+ │ └── invites.js # Invite code management
1021
1033
  ├── ap/
1022
1034
  │ ├── index.js # ActivityPub plugin
1023
1035
  │ ├── keys.js # RSA keypair management
@@ -1030,23 +1042,31 @@ src/
1030
1042
  ├── rdf/
1031
1043
  │ ├── turtle.js # Turtle <-> JSON-LD
1032
1044
  │ └── conneg.js # Content negotiation
1045
+ ├── mashlib/
1046
+ │ └── index.js # Mashlib data browser plugin
1033
1047
  └── utils/
1034
- ├── url.js # URL utilities
1035
- └── conditional.js # If-Match/If-None-Match
1048
+ ├── url.js # URL utilities
1049
+ ├── conditional.js # If-Match/If-None-Match
1050
+ └── ssrf.js # SSRF protection
1036
1051
  ```
1037
1052
 
1038
1053
  ## Dependencies
1039
1054
 
1040
- Minimal dependencies for a fast, secure server:
1055
+ 14 direct dependencies for a fast, secure server:
1041
1056
 
1042
1057
  - **fastify** - High-performance HTTP server
1058
+ - **@fastify/middie** - Express/Connect middleware bridge (for IdP)
1059
+ - **@fastify/rate-limit** - Rate limiting for API endpoints
1043
1060
  - **@fastify/websocket** - WebSocket support for notifications
1061
+ - **@simplewebauthn/server** - Passkey/WebAuthn authentication
1062
+ - **bcryptjs** - Password hashing (pure JS, works on Termux/Android)
1063
+ - **commander** - CLI command parsing
1044
1064
  - **fs-extra** - Enhanced file operations
1045
1065
  - **jose** - JWT/JWK handling for Solid-OIDC
1066
+ - **microfed** - ActivityPub primitives (only when activitypub enabled)
1046
1067
  - **n3** - Turtle parsing (only used when conneg enabled)
1068
+ - **nostr-tools** - Nostr protocol and Schnorr signature verification
1047
1069
  - **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
1070
  - **sql.js** - SQLite storage for federation data (WASM, cross-platform)
1051
1071
 
1052
1072
  ## License
package/bin/jss.js CHANGED
@@ -105,6 +105,8 @@ program
105
105
 
106
106
  // Create and start server
107
107
  const server = createServer({
108
+ port: config.port,
109
+ host: config.host,
108
110
  logger: config.logger,
109
111
  conneg: config.conneg,
110
112
  notifications: config.notifications,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.85",
3
+ "version": "0.0.87",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/config.js CHANGED
@@ -228,6 +228,11 @@ export async function loadConfig(cliOptions = {}, configFile = null) {
228
228
  config.logger = false;
229
229
  }
230
230
 
231
+ // Mashlib requires content negotiation for Turtle support
232
+ if (config.mashlib || config.mashlibCdn) {
233
+ config.conneg = true;
234
+ }
235
+
231
236
  // Validate SSL config
232
237
  if ((config.sslKey && !config.sslCert) || (!config.sslKey && config.sslCert)) {
233
238
  throw new Error('Both --ssl-key and --ssl-cert must be provided together');
@@ -40,6 +40,7 @@ export function handleWebSocket(socket, request, webId = null) {
40
40
  // Store webId and server info on socket for ACL checks
41
41
  socket.webId = webId;
42
42
  socket.serverOrigin = `${request.protocol}://${request.hostname}`;
43
+ socket.publicMode = request.config?.public || false;
43
44
 
44
45
  // Send protocol greeting
45
46
  socket.send('protocol solid-0.1');
@@ -123,6 +124,11 @@ async function checkSubscriptionAccess(url, socket) {
123
124
  const stats = await storage.stat(resourcePath);
124
125
  const isContainer = stats?.isDirectory || resourcePath.endsWith('/');
125
126
 
127
+ // Skip WAC check in public mode
128
+ if (socket.publicMode) {
129
+ return true;
130
+ }
131
+
126
132
  // Check WAC read permission
127
133
  const { allowed } = await checkAccess({
128
134
  resourceUrl: url,
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']);
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Live Reload Test
3
+ *
4
+ * Tests the full chain: file change → WebSocket pub → client receives
5
+ */
6
+
7
+ import { createServer } from '../src/server.js';
8
+ import { writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
9
+ import { join } from 'path';
10
+ import WebSocket from 'ws';
11
+
12
+ const TEST_PORT = 9876;
13
+ const TEST_DIR = '/tmp/live-reload-test-suite';
14
+ const BASE_URL = `http://localhost:${TEST_PORT}`;
15
+
16
+ // Setup and teardown
17
+ function setupTestDir() {
18
+ if (existsSync(TEST_DIR)) {
19
+ rmSync(TEST_DIR, { recursive: true });
20
+ }
21
+ mkdirSync(TEST_DIR, { recursive: true });
22
+ writeFileSync(join(TEST_DIR, 'index.html'), '<!DOCTYPE html><html><body>Hello</body></html>');
23
+ writeFileSync(join(TEST_DIR, 'test.txt'), 'initial content');
24
+ }
25
+
26
+ function cleanupTestDir() {
27
+ if (existsSync(TEST_DIR)) {
28
+ rmSync(TEST_DIR, { recursive: true });
29
+ }
30
+ }
31
+
32
+ // Test 1: Verify WebSocket notifications work for HTTP PUT
33
+ async function testHttpPutNotification() {
34
+ console.log('\n=== Test 1: HTTP PUT triggers WebSocket notification ===');
35
+
36
+ setupTestDir();
37
+
38
+ const server = createServer({
39
+ root: TEST_DIR,
40
+ port: TEST_PORT,
41
+ logger: false,
42
+ liveReload: true,
43
+ public: true,
44
+ });
45
+
46
+ await server.listen({ port: TEST_PORT, host: '0.0.0.0' });
47
+ console.log('Server started on port', TEST_PORT);
48
+
49
+ return new Promise((resolve, reject) => {
50
+ const timeout = setTimeout(() => {
51
+ ws.close();
52
+ server.close();
53
+ reject(new Error('Timeout: No WebSocket notification received'));
54
+ }, 5000);
55
+
56
+ const ws = new WebSocket(`ws://localhost:${TEST_PORT}/.notifications`);
57
+
58
+ ws.on('open', () => {
59
+ console.log('WebSocket connected');
60
+ // Subscribe to the test file
61
+ ws.send(`sub ${BASE_URL}/test.txt`);
62
+ console.log('Subscribed to', `${BASE_URL}/test.txt`);
63
+
64
+ // Wait a bit then do HTTP PUT
65
+ setTimeout(async () => {
66
+ console.log('Doing HTTP PUT...');
67
+ const res = await fetch(`${BASE_URL}/test.txt`, {
68
+ method: 'PUT',
69
+ body: 'updated via HTTP',
70
+ headers: { 'Content-Type': 'text/plain' }
71
+ });
72
+ console.log('PUT response:', res.status);
73
+ }, 500);
74
+ });
75
+
76
+ ws.on('message', (data) => {
77
+ const msg = data.toString();
78
+ console.log('WebSocket received:', msg);
79
+
80
+ if (msg.startsWith('pub ')) {
81
+ clearTimeout(timeout);
82
+ console.log('SUCCESS: Received pub notification');
83
+ ws.close();
84
+ server.close().then(() => {
85
+ cleanupTestDir();
86
+ resolve(true);
87
+ });
88
+ }
89
+ });
90
+
91
+ ws.on('error', (err) => {
92
+ clearTimeout(timeout);
93
+ server.close();
94
+ reject(err);
95
+ });
96
+ });
97
+ }
98
+
99
+ // Test 2: Verify file watcher detects filesystem changes
100
+ async function testFileWatcherNotification() {
101
+ console.log('\n=== Test 2: Filesystem change triggers WebSocket notification ===');
102
+
103
+ setupTestDir();
104
+
105
+ const server = createServer({
106
+ root: TEST_DIR,
107
+ port: TEST_PORT,
108
+ logger: false,
109
+ liveReload: true,
110
+ public: true,
111
+ });
112
+
113
+ await server.listen({ port: TEST_PORT, host: '0.0.0.0' });
114
+ console.log('Server started on port', TEST_PORT);
115
+
116
+ return new Promise((resolve, reject) => {
117
+ const timeout = setTimeout(() => {
118
+ ws.close();
119
+ server.close();
120
+ reject(new Error('Timeout: No WebSocket notification received for filesystem change'));
121
+ }, 5000);
122
+
123
+ const ws = new WebSocket(`ws://localhost:${TEST_PORT}/.notifications`);
124
+
125
+ ws.on('open', () => {
126
+ console.log('WebSocket connected');
127
+ // Subscribe to the test file
128
+ ws.send(`sub ${BASE_URL}/test.txt`);
129
+ console.log('Subscribed to', `${BASE_URL}/test.txt`);
130
+
131
+ // Wait a bit then modify file directly on filesystem
132
+ setTimeout(() => {
133
+ console.log('Modifying file via filesystem...');
134
+ writeFileSync(join(TEST_DIR, 'test.txt'), 'updated via filesystem ' + Date.now());
135
+ console.log('File written');
136
+ }, 1000);
137
+ });
138
+
139
+ ws.on('message', (data) => {
140
+ const msg = data.toString();
141
+ console.log('WebSocket received:', msg);
142
+
143
+ if (msg.startsWith('pub ')) {
144
+ clearTimeout(timeout);
145
+ console.log('SUCCESS: Received pub notification for filesystem change');
146
+ ws.close();
147
+ server.close().then(() => {
148
+ cleanupTestDir();
149
+ resolve(true);
150
+ });
151
+ }
152
+ });
153
+
154
+ ws.on('error', (err) => {
155
+ clearTimeout(timeout);
156
+ server.close();
157
+ reject(err);
158
+ });
159
+ });
160
+ }
161
+
162
+ // Test 3: Verify fs.watch works on this platform
163
+ async function testFsWatch() {
164
+ console.log('\n=== Test 3: Basic fs.watch functionality ===');
165
+
166
+ const { watch } = await import('fs');
167
+ const testFile = '/tmp/fswatch-test.txt';
168
+
169
+ writeFileSync(testFile, 'initial');
170
+
171
+ return new Promise((resolve, reject) => {
172
+ const timeout = setTimeout(() => {
173
+ watcher.close();
174
+ reject(new Error('fs.watch did not detect file change'));
175
+ }, 3000);
176
+
177
+ const watcher = watch(testFile, (eventType, filename) => {
178
+ console.log('fs.watch detected:', eventType, filename);
179
+ clearTimeout(timeout);
180
+ watcher.close();
181
+ resolve(true);
182
+ });
183
+
184
+ // Modify file after short delay
185
+ setTimeout(() => {
186
+ console.log('Modifying file...');
187
+ writeFileSync(testFile, 'modified ' + Date.now());
188
+ }, 500);
189
+ });
190
+ }
191
+
192
+ // Test 4: Verify fs.watch with recursive option
193
+ async function testFsWatchRecursive() {
194
+ console.log('\n=== Test 4: fs.watch with recursive option ===');
195
+
196
+ const { watch } = await import('fs');
197
+ const testDir = '/tmp/fswatch-recursive-test';
198
+
199
+ if (existsSync(testDir)) rmSync(testDir, { recursive: true });
200
+ mkdirSync(testDir, { recursive: true });
201
+ writeFileSync(join(testDir, 'file.txt'), 'initial');
202
+
203
+ return new Promise((resolve, reject) => {
204
+ const timeout = setTimeout(() => {
205
+ watcher.close();
206
+ console.log('FAIL: fs.watch recursive did not detect file change');
207
+ resolve(false); // Don't reject, just report failure
208
+ }, 3000);
209
+
210
+ let detected = false;
211
+ const watcher = watch(testDir, { recursive: true }, (eventType, filename) => {
212
+ if (!detected) {
213
+ detected = true;
214
+ console.log('fs.watch recursive detected:', eventType, filename);
215
+ clearTimeout(timeout);
216
+ watcher.close();
217
+ resolve(true);
218
+ }
219
+ });
220
+
221
+ watcher.on('error', (err) => {
222
+ console.log('fs.watch error:', err.message);
223
+ clearTimeout(timeout);
224
+ resolve(false);
225
+ });
226
+
227
+ // Modify file after short delay
228
+ setTimeout(() => {
229
+ console.log('Modifying file in watched directory...');
230
+ writeFileSync(join(testDir, 'file.txt'), 'modified ' + Date.now());
231
+ }, 500);
232
+ });
233
+ }
234
+
235
+ // Run all tests
236
+ async function runTests() {
237
+ console.log('Live Reload Test Suite');
238
+ console.log('======================');
239
+
240
+ try {
241
+ // Test basic fs.watch first
242
+ const fsWatchWorks = await testFsWatch();
243
+ console.log('Test 3 result: fs.watch works =', fsWatchWorks);
244
+
245
+ // Test recursive fs.watch
246
+ const fsWatchRecursiveWorks = await testFsWatchRecursive();
247
+ console.log('Test 4 result: fs.watch recursive works =', fsWatchRecursiveWorks);
248
+
249
+ // Test HTTP PUT notification
250
+ await testHttpPutNotification();
251
+ console.log('Test 1 result: PASSED');
252
+
253
+ // Test file watcher notification
254
+ await testFileWatcherNotification();
255
+ console.log('Test 2 result: PASSED');
256
+
257
+ console.log('\n=== All tests passed ===');
258
+ } catch (err) {
259
+ console.error('\n=== Test FAILED ===');
260
+ console.error(err.message);
261
+ process.exit(1);
262
+ }
263
+ }
264
+
265
+ runTests();
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>