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.
- package/.claude/settings.local.json +2 -1
- package/README.md +45 -23
- package/bin/jss.js +1 -0
- package/package.json +1 -1
- package/src/auth/did-nostr.js +33 -6
- package/src/config.js +14 -0
- package/src/server.js +16 -1
- package/src/wac/parser.js +3 -2
- package/test/wac.test.js +17 -0
- package/fonstr-data/index.html +0 -120
package/README.md
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# JavaScript Solid Server
|
|
2
2
|
|
|
3
|
+
[](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
|
|
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) |
|
|
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: **
|
|
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
|
|
992
|
-
│ ├── token.js
|
|
993
|
-
│ ├── solid-oidc.js
|
|
994
|
-
│ ├── nostr.js
|
|
995
|
-
│ ├── did-nostr.js
|
|
996
|
-
│ └── webid-tls.js
|
|
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
|
|
1014
|
-
│ ├── provider.js
|
|
1015
|
-
│ ├── adapter.js
|
|
1016
|
-
│ ├── accounts.js
|
|
1017
|
-
│ ├──
|
|
1018
|
-
│ ├──
|
|
1019
|
-
│ ├──
|
|
1020
|
-
│
|
|
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
|
|
1035
|
-
|
|
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
|
-
|
|
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
package/src/auth/did-nostr.js
CHANGED
|
@@ -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
|
|
48
|
-
|
|
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
|
-
//
|
|
97
|
-
|
|
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
|
-
|
|
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.
|
|
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#' },
|
package/fonstr-data/index.html
DELETED
|
@@ -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>
|