summaryrefslogtreecommitdiffstats
path: root/internal/handler
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-13 20:59:53 -0800
committerbndw <ben@bdw.to>2026-02-13 20:59:53 -0800
commit18f5906b88800dadd1f1dd06cec58fd568e8f75c (patch)
treebf7eea61781b04d4541add624813d0a24c38d4e9 /internal/handler
parent98a798f0cbd64ff0f003746e8b33c6666c61de64 (diff)
redesign: early web minimalism + cypherpunk
Complete aesthetic reversal - from cyber-brutalist to minimal: AESTHETIC: 1995 web document meets cypherpunks mailing list - Courier New system font (no web fonts) - Black on white for readability - Semantic HTML with minimal CSS - PGP signature blocks (cypherpunk heritage) - Classic underlined blue links - Horizontal rules for section breaks - Looks like a .txt file rendered as HTML PHILOSOPHY: Throwback to when the web was just documents. No animations, no grids, no gradients. Just semantic HTML, monospace type, and information. Fast loading, accessible, timeless. CYPHERPUNK TOUCHES: - PGP signature blocks - Hash fingerprints - Technical language - Cryptographic references - Feels like reading crypto mailing list archives Zero frameworks, zero build tools, maximum signal.
Diffstat (limited to 'internal/handler')
-rw-r--r--internal/handler/websocket/index.go523
1 files changed, 115 insertions, 408 deletions
diff --git a/internal/handler/websocket/index.go b/internal/handler/websocket/index.go
index 861fbb1..887b6ea 100644
--- a/internal/handler/websocket/index.go
+++ b/internal/handler/websocket/index.go
@@ -10,441 +10,148 @@ var indexTemplate = template.Must(template.New("index").Parse(`<!DOCTYPE html>
10<head> 10<head>
11 <meta charset="UTF-8"> 11 <meta charset="UTF-8">
12 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 12 <meta name="viewport" content="width=device-width, initial-scale=1.0">
13 <title>NOSTR-GRPC-RELAY/v1.0.0</title> 13 <title>Nostr Relay</title>
14 <link rel="preconnect" href="https://fonts.googleapis.com">
15 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16 <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
17 <style> 14 <style>
18 :root {
19 --bg-primary: #0a0e14;
20 --bg-secondary: #151a23;
21 --bg-tertiary: #1f2531;
22 --cyan: #00ff9f;
23 --green: #39ff14;
24 --white: #e6e6e6;
25 --gray: #6b7280;
26 --red: #ff3366;
27 --border: #2a3142;
28 }
29
30 * {
31 margin: 0;
32 padding: 0;
33 box-sizing: border-box;
34 }
35
36 body { 15 body {
37 font-family: 'JetBrains Mono', monospace; 16 font-family: 'Courier New', Courier, monospace;
38 background: var(--bg-primary); 17 background: #fff;
39 color: var(--white); 18 color: #000;
40 min-height: 100vh; 19 max-width: 720px;
41 overflow-x: hidden; 20 margin: 40px auto;
42 position: relative; 21 padding: 0 20px;
43 } 22 line-height: 1.6;
44 23 }
45 /* Animated grid background */ 24 h1 {
46 body::before { 25 font-size: 18px;
47 content: ''; 26 font-weight: normal;
48 position: fixed; 27 margin: 20px 0 10px 0;
49 top: 0; 28 }
50 left: 0; 29 h2 {
51 width: 100%; 30 font-size: 16px;
52 height: 100%; 31 font-weight: normal;
53 background-image: 32 margin: 30px 0 10px 0;
54 repeating-linear-gradient(0deg, transparent, transparent 2px, var(--border) 2px, var(--border) 3px), 33 }
55 repeating-linear-gradient(90deg, transparent, transparent 2px, var(--border) 2px, var(--border) 3px); 34 h3 {
56 background-size: 50px 50px; 35 font-size: 14px;
57 opacity: 0.15; 36 font-weight: normal;
58 pointer-events: none; 37 margin: 20px 0 5px 0;
59 animation: gridScroll 20s linear infinite; 38 }
60 z-index: 0; 39 p, li {
61 } 40 font-size: 13px;
62 41 }
63 @keyframes gridScroll { 42 a {
64 0% { transform: translate(0, 0); } 43 color: #00e;
65 100% { transform: translate(50px, 50px); } 44 text-decoration: underline;
66 } 45 }
67 46 a:visited {
68 .container { 47 color: #551a8b;
69 position: relative; 48 }
70 z-index: 1; 49 hr {
71 max-width: 1200px; 50 border: none;
72 margin: 0 auto; 51 border-top: 1px solid #000;
73 padding: 2rem; 52 margin: 30px 0;
74 } 53 }
75 54 pre {
76 /* Status bar */ 55 font-size: 12px;
77 .status-bar { 56 line-height: 1.4;
78 display: flex; 57 overflow-x: auto;
79 justify-content: space-between; 58 }
80 align-items: center; 59 code {
81 padding: 1rem; 60 font-family: 'Courier New', Courier, monospace;
82 background: var(--bg-secondary); 61 font-size: 13px;
83 border: 1px solid var(--border); 62 }
84 margin-bottom: 2rem; 63 .hash {
85 font-size: 0.75rem; 64 color: #666;
86 font-weight: 300; 65 font-size: 11px;
87 animation: fadeIn 0.5s ease-out; 66 }
88 } 67 ul {
89 68 padding-left: 20px;
90 .status-item { 69 }
91 display: flex; 70 .pgp-block {
92 align-items: center; 71 margin: 20px 0;
93 gap: 0.5rem; 72 padding: 10px;
94 } 73 border: 1px solid #ccc;
95 74 background: #f5f5f5;
96 .status-indicator {
97 width: 8px;
98 height: 8px;
99 background: var(--green);
100 border-radius: 50%;
101 animation: pulse 2s ease-in-out infinite;
102 }
103
104 @keyframes pulse {
105 0%, 100% { opacity: 1; }
106 50% { opacity: 0.4; }
107 }
108
109 /* Header with ASCII art */
110 .header {
111 margin-bottom: 3rem;
112 animation: fadeIn 0.8s ease-out 0.2s both;
113 }
114
115 .ascii-logo {
116 font-size: 0.5rem;
117 line-height: 0.5rem;
118 color: var(--cyan);
119 white-space: pre;
120 margin-bottom: 1.5rem;
121 filter: drop-shadow(0 0 10px rgba(0, 255, 159, 0.3));
122 animation: glitchLogo 3s ease-in-out infinite;
123 }
124
125 @keyframes glitchLogo {
126 0%, 90%, 100% { transform: translate(0); }
127 92% { transform: translate(-2px, 1px); }
128 94% { transform: translate(2px, -1px); }
129 96% { transform: translate(-1px, 2px); }
130 }
131
132 .header h1 {
133 font-size: 2rem;
134 font-weight: 700;
135 color: var(--cyan);
136 margin-bottom: 0.5rem;
137 letter-spacing: 0.1em;
138 }
139
140 .header .subtitle {
141 color: var(--gray);
142 font-size: 0.875rem;
143 font-weight: 300;
144 }
145
146 .header .subtitle::before {
147 content: '> ';
148 color: var(--green);
149 }
150
151 /* Protocol blocks */
152 .protocols {
153 display: grid;
154 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
155 gap: 1.5rem;
156 margin-bottom: 3rem;
157 }
158
159 .protocol-card {
160 background: var(--bg-secondary);
161 border: 1px solid var(--border);
162 padding: 1.5rem;
163 position: relative;
164 overflow: hidden;
165 animation: fadeInUp 0.8s ease-out backwards;
166 }
167
168 .protocol-card:nth-child(1) { animation-delay: 0.3s; }
169 .protocol-card:nth-child(2) { animation-delay: 0.4s; }
170 .protocol-card:nth-child(3) { animation-delay: 0.5s; }
171
172 .protocol-card::before {
173 content: '';
174 position: absolute;
175 top: 0;
176 left: 0;
177 width: 100%;
178 height: 2px;
179 background: linear-gradient(90deg, var(--cyan), transparent);
180 animation: scanLine 2s ease-in-out infinite;
181 }
182
183 @keyframes scanLine {
184 0%, 100% { transform: translateX(-100%); }
185 50% { transform: translateX(100%); }
186 }
187
188 .protocol-card:hover {
189 border-color: var(--cyan);
190 box-shadow: 0 0 20px rgba(0, 255, 159, 0.1);
191 transition: all 0.3s ease;
192 }
193
194 .protocol-header {
195 display: flex;
196 justify-content: space-between;
197 align-items: center;
198 margin-bottom: 1rem;
199 }
200
201 .protocol-name {
202 font-size: 0.875rem;
203 font-weight: 700;
204 color: var(--green);
205 letter-spacing: 0.05em;
206 }
207
208 .protocol-status {
209 font-size: 0.75rem;
210 color: var(--gray);
211 display: flex;
212 align-items: center;
213 gap: 0.5rem;
214 }
215
216 .protocol-status::before {
217 content: '●';
218 color: var(--green);
219 } 75 }
76 </style>
77</head>
78<body>
220 79
221 .protocol-endpoint { 80<pre>
222 background: var(--bg-tertiary); 81-----BEGIN NOSTR RELAY INFO-----
223 padding: 0.75rem; 82Hash: SHA256
224 margin: 1rem 0;
225 border-left: 2px solid var(--cyan);
226 font-size: 0.75rem;
227 color: var(--cyan);
228 word-break: break-all;
229 font-weight: 400;
230 }
231 83
232 .protocol-desc { 84relay_id: nostr-grpc-relay
233 font-size: 0.75rem; 85version: 1.0.0
234 color: var(--gray); 86status: online
235 font-weight: 300; 87-----END NOSTR RELAY INFO-----
236 line-height: 1.5; 88</pre>
237 }
238 89
239 /* Info section */ 90<h1>Nostr Relay</h1>
240 .info-grid {
241 display: grid;
242 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
243 gap: 1.5rem;
244 margin-bottom: 3rem;
245 }
246 91
247 .info-card { 92<p>This relay implements the Nostr protocol with support for multiple transport layers. Connect using gRPC, HTTP/JSON (Connect), or standard WebSocket.</p>
248 background: var(--bg-secondary);
249 border: 1px solid var(--border);
250 padding: 1.5rem;
251 animation: fadeInUp 0.8s ease-out 0.6s backwards;
252 }
253 93
254 .info-card h3 { 94<hr>
255 font-size: 0.75rem;
256 color: var(--cyan);
257 margin-bottom: 1rem;
258 letter-spacing: 0.1em;
259 font-weight: 700;
260 }
261 95
262 .nip-list { 96<h2>Connection Endpoints</h2>
263 display: flex;
264 flex-wrap: wrap;
265 gap: 0.5rem;
266 }
267 97
268 .nip-tag { 98<h3>gRPC (Native Binary)</h3>
269 background: var(--bg-tertiary); 99<p><code>{{.GrpcAddr}}</code></p>
270 border: 1px solid var(--border); 100<p>High-performance binary protocol using Protocol Buffers over HTTP/2. For applications requiring low latency and high throughput.</p>
271 padding: 0.25rem 0.75rem;
272 font-size: 0.75rem;
273 color: var(--green);
274 font-weight: 500;
275 transition: all 0.2s ease;
276 }
277 101
278 .nip-tag:hover { 102<h3>Connect (HTTP/JSON)</h3>
279 border-color: var(--green); 103<p><code>{{.HttpAddr}}/nostr.v1.NostrRelay/*</code></p>
280 box-shadow: 0 0 10px rgba(57, 255, 20, 0.2); 104<p>Browser-compatible HTTP interface. Standard JSON over HTTP/1.1 or HTTP/2. CORS enabled.</p>
281 }
282 105
283 .feature-list { 106<h3>WebSocket (Nostr Protocol)</h3>
284 list-style: none; 107<p><code>ws://{{.WsAddr}}/</code></p>
285 font-size: 0.75rem; 108<p>Standard Nostr protocol implementation (NIP-01). Compatible with all Nostr clients: Damus, Amethyst, Snort, Iris, etc.</p>
286 color: var(--gray);
287 font-weight: 300;
288 }
289 109
290 .feature-list li { 110<hr>
291 padding: 0.5rem 0;
292 border-bottom: 1px solid var(--border);
293 }
294 111
295 .feature-list li:last-child { 112<h2>Supported NIPs</h2>
296 border-bottom: none;
297 }
298 113
299 .feature-list li::before { 114<ul>
300 content: '[✓] '; 115<li><strong>NIP-01</strong> - Basic protocol flow (EVENT, REQ, CLOSE)</li>
301 color: var(--green); 116<li><strong>NIP-09</strong> - Event deletion</li>
302 margin-right: 0.5rem; 117<li><strong>NIP-11</strong> - Relay information document</li>
303 } 118</ul>
304 119
305 /* Footer */ 120<hr>
306 .footer {
307 text-align: center;
308 padding: 2rem;
309 border-top: 1px solid var(--border);
310 margin-top: 3rem;
311 font-size: 0.75rem;
312 color: var(--gray);
313 animation: fadeIn 0.8s ease-out 0.8s both;
314 }
315 121
316 .footer pre { 122<h2>Implementation Details</h2>
317 margin: 1rem 0;
318 color: var(--cyan);
319 opacity: 0.5;
320 font-size: 0.5rem;
321 line-height: 0.6rem;
322 }
323 123
324 @keyframes fadeIn { 124<p><strong>Storage:</strong> SQLite with Write-Ahead Logging (WAL mode)<br>
325 from { opacity: 0; } 125<strong>Event Format:</strong> Binary-first (Protocol Buffers + compressed JSON)<br>
326 to { opacity: 1; } 126<strong>Validation:</strong> Full cryptographic signature and event ID verification<br>
327 } 127<strong>Subscriptions:</strong> Real-time event streaming with filter matching<br>
128<strong>Deletion:</strong> Hard delete (events are permanently removed)</p>
328 129
329 @keyframes fadeInUp { 130<hr>
330 from {
331 opacity: 0;
332 transform: translateY(20px);
333 }
334 to {
335 opacity: 1;
336 transform: translateY(0);
337 }
338 }
339 131
340 @media (max-width: 768px) { 132<h2>Technical Notes</h2>
341 .container {
342 padding: 1rem;
343 }
344 .ascii-logo {
345 font-size: 0.35rem;
346 line-height: 0.4rem;
347 }
348 .header h1 {
349 font-size: 1.5rem;
350 }
351 .status-bar {
352 flex-direction: column;
353 gap: 0.5rem;
354 align-items: flex-start;
355 }
356 }
357 </style>
358</head>
359<body>
360 <div class="container">
361 <div class="status-bar">
362 <div class="status-item">
363 <span class="status-indicator"></span>
364 <span>RELAY.ONLINE</span>
365 </div>
366 <div class="status-item">
367 <span>PROTOCOLS: 3</span>
368 </div>
369 <div class="status-item">
370 <span>NIPs: 01/09/11</span>
371 </div>
372 </div>
373 133
374 <div class="header"> 134<p>Events are stored in binary format using Protocol Buffers alongside compressed canonical JSON. This dual-storage approach provides both performance (binary queries) and compatibility (JSON export).</p>
375 <pre class="ascii-logo">
376 ███▄ █ ▒█████ ██████ ▄▄▄█████▓ ██▀███
377 ██ ▀█ █ ▒██▒ ██▒▒██ ▒ ▓ ██▒ ▓▒▓██ ▒ ██▒
378▓██ ▀█ ██▒▒██░ ██▒░ ▓██▄ ▒ ▓██░ ▒░▓██ ░▄█ ▒
379▓██▒ ▐▌██▒▒██ ██░ ▒ ██▒░ ▓██▓ ░ ▒██▀▀█▄
380▒██░ ▓██░░ ████▓▒░▒██████▒▒ ▒██▒ ░ ░██▓ ▒██▒
381░ ▒░ ▒ ▒ ░ ▒░▒░▒░ ▒ ▒▓▒ ▒ ░ ▒ ░░ ░ ▒▓ ░▒▓░
382░ ░░ ░ ▒░ ░ ▒ ▒░ ░ ░▒ ░ ░ ░ ░▒ ░ ▒░
383 ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░
384 ░ ░ ░ ░ ░ </pre>
385 <h1>NOSTR-GRPC-RELAY</h1>
386 <p class="subtitle">decentralized protocol node // multi-transport relay</p>
387 </div>
388 135
389 <div class="protocols"> 136<p>The relay validates all events before storage:</p>
390 <div class="protocol-card"> 137<pre>1. Verify event ID matches SHA256(canonical_json)
391 <div class="protocol-header"> 1382. Verify schnorr signature against pubkey
392 <span class="protocol-name">gRPC</span> 1393. Store in SQLite with indexed queries</pre>
393 <span class="protocol-status">ACTIVE</span>
394 </div>
395 <div class="protocol-endpoint">{{.GrpcAddr}}</div>
396 <p class="protocol-desc">Native binary protocol. High-throughput RPC interface for application-layer integration. Protocol Buffers over HTTP/2.</p>
397 </div>
398 140
399 <div class="protocol-card"> 141<p>Subscriptions use a shared manager across all three protocols. An event published via gRPC will be immediately broadcast to WebSocket subscribers and vice versa.</p>
400 <div class="protocol-header">
401 <span class="protocol-name">CONNECT</span>
402 <span class="protocol-status">ACTIVE</span>
403 </div>
404 <div class="protocol-endpoint">{{.HttpAddr}}/nostr.v1.NostrRelay/*</div>
405 <p class="protocol-desc">Browser-compatible HTTP/JSON interface. gRPC-Web with Connect protocol. CORS-enabled for web clients.</p>
406 </div>
407 142
408 <div class="protocol-card"> 143<hr>
409 <div class="protocol-header">
410 <span class="protocol-name">WEBSOCKET</span>
411 <span class="protocol-status">ACTIVE</span>
412 </div>
413 <div class="protocol-endpoint">ws://{{.WsAddr}}/</div>
414 <p class="protocol-desc">Standard Nostr protocol (NIP-01). Compatible with all Nostr clients. Real-time event streaming.</p>
415 </div>
416 </div>
417 144
418 <div class="info-grid"> 145<div class="pgp-block">
419 <div class="info-card"> 146<pre>-----BEGIN PGP SIGNATURE-----
420 <h3>SUPPORTED_NIPS</h3> 147<span class="hash">relay_fingerprint: a8f9c2e1d4b7a3f6</span>
421 <div class="nip-list"> 148<span class="hash">protocol_version: nostr_01</span>
422 <span class="nip-tag">NIP-01</span> 149<span class="hash">transport_types: grpc|connect|websocket</span>
423 <span class="nip-tag">NIP-09</span> 150-----END PGP SIGNATURE-----</pre>
424 <span class="nip-tag">NIP-11</span> 151</div>
425 </div>
426 </div>
427 152
428 <div class="info-card"> 153<p><small>This relay runs on open-source software. Decentralized, censorship-resistant, user-owned.</small></p>
429 <h3>RELAY_FEATURES</h3>
430 <ul class="feature-list">
431 <li>Binary-first storage</li>
432 <li>SQLite WAL mode</li>
433 <li>Event validation</li>
434 <li>Real-time subscriptions</li>
435 <li>Hard deletion (NIP-09)</li>
436 </ul>
437 </div>
438 </div>
439 154
440 <div class="footer">
441 <pre>
442┌─────────────────────────────────────┐
443│ DECENTRALIZED // CENSORSHIP-RESISTANT │
444└─────────────────────────────────────┘</pre>
445 <p>relay.status = online // uptime.monitor = true</p>
446 </div>
447 </div>
448</body> 155</body>
449</html>`)) 156</html>`))
450 157