| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
| |
The AuthOperationConfig struct was missing YAML tags, causing
the config loader to not properly parse allowed_npubs from YAML.
This was causing TestNpubNormalization to fail with an index out
of range panic because AllowedNpubs was always empty.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
- Send OK false for rate limit errors instead of NOTICE
- Send OK false for auth errors (e.g. pubkey not in allowlist)
- Remove OK response for AUTH events (AUTH is not an EVENT type)
- Parse event before auth checks to get event ID for error responses
These changes improve client UX by providing immediate, structured
feedback for all rejection cases instead of generic NOTICE messages.
The AUTH event OK removal fixes a bug where clients would read the
wrong response when sending EVENT after AUTH.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Add comprehensive WebSocket handler integration tests that verify:
- NIP-42 authentication flow (auth required, challenge/response)
- Allowlist enforcement (reject unauthorized pubkeys)
- Rate limiting by IP address
- Rate limiting by authenticated pubkey
- No-auth mode works correctly
These tests use real WebSocket connections and would have caught
the AUTH timeout bug and other protocol issues.
Tests cover:
- TestAuthRequired: Verifies AUTH challenge sent, client authenticates, publish succeeds
- TestAuthNotInAllowlist: Verifies pubkeys not in allowlist are rejected
- TestRateLimitByIP: Verifies unauthenticated clients are rate limited by IP
- TestRateLimitByPubkey: Verifies authenticated clients are rate limited by pubkey
- TestNoAuthWhenDisabled: Verifies publishing works when auth is disabled
|
| |
|
|
|
|
|
|
| |
- Remove blocking wait for AUTH challenge on connection
- Add channel-based synchronization for AUTH completion
- Retry publish after AUTH completes successfully
- Support both auth-required and non-auth relay configurations
- Add clarifying comments for auth flow handling
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Previously when an EVENT was received before authentication was
completed, the relay would send an AUTH challenge but silently drop
the EVENT without sending an OK response. This caused clients to
timeout waiting for the OK that never came.
Now the relay immediately sends OK false with "auth-required" message,
so clients know the EVENT was rejected and can retry after AUTH
completes.
Also optimized to parse the event JSON only once instead of twice.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Track and display blocked event counts with per-kind breakdown.
- Add events_blocked_total counter with kind label
- Add RecordBlockedEvent method to metrics
- Display blocked count in dashboard Storage section
- Call metric when rejecting non-core protocol kinds
Allows monitoring spam patterns and verifying kind filter effectiveness.
Raw metrics show breakdown by kind (e.g., kind=20001, kind=30311).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Add allowlist filtering to reject spam, ephemeral events, and live chat
messages. Only accept core protocol kinds (notes, reactions, metadata, etc).
Allowed kinds:
- 0, 1, 3, 4, 5, 6, 7: Core protocol (NIP-01, 02, 04, 09, 18, 25)
- 9735: Zaps (NIP-57)
- 10000-10002: Mute/pin lists (NIP-51)
- 10050: Relay list metadata
- 30023, 30078: Long-form content, app data (NIP-23, 78)
Rejected kinds:
- 20001: Bot metadata spam (~157+ events/day)
- 30311: Live chat messages (~100+ events/day)
- All other kinds: Future spam/ephemeral events
Reduces storage growth by ~80% while keeping all essential functionality.
Clients receive "rejected: kind not supported" for filtered events.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Add request tracking for EVENT and REQ messages to match gRPC behavior.
Dashboard now shows total/success/error counts for all requests.
- Add RecordRequest to MetricsRecorder interface
- Track timing and status in handleEvent and handleReq
- Record metrics with status: ok, error, unauthenticated, rate_limited
- Measure request duration for performance monitoring
WebSocket is the primary interface, so tracking these requests is critical
for understanding relay usage and performance.
|
| |
|
|
|
|
|
|
|
|
|
| |
Connection metrics were defined but never called. Add IncrementConnections
and DecrementConnections to MetricsRecorder interface and call them when
WebSocket connections are established/closed.
- Add connection methods to MetricsRecorder interface
- Increment on successful WebSocket upgrade
- Decrement in defer when connection closes
- Dashboard will now show active connection count
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
WebSocket clients were completely unprotected from abuse. Add RateLimiter
interface to WebSocket handler and enforce limits on EVENT and REQ messages.
- Add RateLimiter interface with Allow(identifier, method) method
- Track client IP in connState (proxy-aware via X-Forwarded-For)
- Check rate limits in handleEvent and handleReq
- Use authenticated pubkey as identifier, fallback to IP
- Share same rate limiter instance with gRPC
- Add getClientIP() helper that checks proxy headers first
Critical security fix for production deployment. Without this, any client
could spam unlimited events/subscriptions via WebSocket.
|
| |
|
|
|
|
|
|
|
| |
Check X-Forwarded-For and X-Real-IP headers before peer info to correctly
identify clients behind reverse proxies. Previously, rate limiting would
apply globally when behind Caddy/nginx because all requests appeared to
come from the proxy's IP address.
This fix is critical for production deployments behind reverse proxies.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Replace runtime type assertions with compile-time safe AuthStore interface.
Add connState struct for cleaner per-connection state management instead
of mutable pointer parameters. Reduce auth challenge TTL from 10min to 2min.
- Add AuthStore interface with CreateAuthChallenge and ValidateAndConsumeChallenge
- Add connState struct for authenticatedPubkey and authChallenge
- Remove fragile type assertion pattern in requireAuth and handleAuth
- Add nil checks for auth store with clear error messages
- Update Handler to have separate auth field
- Wire auth store in main.go when auth is enabled
|
| |
|
|
|
|
|
| |
After sending AUTH challenge, return nil instead of error to avoid
sending NOTICE messages to clients. Add explicit checks in handleEvent
and handleReq to silently ignore requests when auth is required but
client hasn't authenticated yet. This follows NIP-42 spec more closely.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Add support for authenticating WebSocket clients using NIP-42 protocol,
enabling auth restrictions for normal Nostr clients.
Storage layer (internal/storage/auth.go):
- CreateAuthChallenge() - Generate random 32-byte challenge with 10min TTL
- ValidateAndConsumeChallenge() - Verify challenge validity and mark as used
- CleanupExpiredChallenges() - Remove old challenges from database
- Uses existing auth_challenges table
WebSocket handler (internal/handler/websocket/handler.go):
- Track authenticatedPubkey per connection
- Track authChallenge per connection
- requireAuth() - Check if operation requires authentication
- handleAuth() - Process AUTH responses (kind 22242 events)
- sendAuthChallenge() - Send AUTH challenge to client
- Enforce auth on EVENT (writes) and REQ (reads) messages
- Support separate read/write allowlists
Main (cmd/relay/main.go):
- Wire auth config from YAML to WebSocket handler
- Pass read/write enabled flags and allowed npub lists
NIP-42 Flow:
1. Client sends EVENT/REQ without auth
2. If auth required, relay sends: ["AUTH", "<challenge>"]
3. Client signs kind 22242 event with challenge tag
4. Client sends: ["AUTH", <signed-event>]
5. Relay validates signature, challenge, and allowlist
6. Connection marked as authenticated
7. Client can now EVENT/REQ
Example config to restrict writes to your npub:
```yaml
auth:
write:
enabled: true
allowed_npubs:
- npub1your-npub-here...
```
WebSocket clients (Damus, Amethyst, etc.) can now authenticate!
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Transform metrics dashboard from dark modern theme to classic beige
theme matching the index page.
Changes:
- Background: dark (#0a0a0a) → beige (#f5f0e8)
- Font: sans-serif → Courier New (monospace)
- Colors: vibrant gradients → simple black/brown
- Cards: dark (#1a1a1a) → white with black borders
- Layout: max-width 1200px → 960px (matches index)
- Typography: modern sizing → classic smaller fonts
- Logo: Add muxstr SVG logo matching index page
- Dividers: Add horizontal rules for section breaks
- Status badge: Green gradient → orange/brown (#c97556)
- Removed: Color coding (success/error/warning classes)
Result: Clean, classic, brutalist aesthetic consistent with index page.
The dashboard now feels like part of the same relay, not a separate app.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Track and display storage and performance metrics that were previously
showing zeros.
Storage metrics:
- Add GetStats() method to storage returning event count and DB size
- Store database file path for size calculation
- Update metrics every 30 seconds via goroutine in main.go
- Display event count and DB size (MB) in dashboard
Performance metrics:
- Calculate average latency from histogram sum/count metrics
- Display as milliseconds in dashboard
- Formula: (duration_sum / duration_count) * 1000
Missing metrics (deferred):
- Connections: requires connection lifecycle tracking (gRPC + WebSocket)
- Deletions: count would need API change to ProcessDeletion
Dashboard now shows accurate storage stats and request latency!
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Add metrics tracking for WebSocket (NIP-01) subscriptions in addition
to existing gRPC subscription tracking.
Changes:
- Add Count() method to subscription.Manager
- Add MetricsRecorder interface to WebSocket handler
- Update subscription metrics when REQ/CLOSE messages processed
- Wire up metrics to WebSocket handler in main.go
Before: Only gRPC stream subscriptions were counted
After: Both gRPC and WebSocket subscriptions tracked accurately
This fixes the dashboard showing 0 subscriptions when clients connect
via WebSocket (e.g., nak req --stream).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Bug: Dashboard showed zeros because it detected "go" as the prefix instead
of "muxstr" when Go runtime metrics appeared first in the metrics list.
Fix: Search for the first metric containing "_relay_" to properly detect
the namespace (muxstr, nostr-grpc, etc.) instead of using the first metric
in the alphabetical list.
Before: Object.keys(metrics)[0] → "go_gc_..." → prefix = "go"
After: Find metric with "_relay_" → "muxstr_relay_..." → prefix = "muxstr"
This fixes the dashboard displaying zeros when metrics data is present.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Add config.example.yaml based on .ship deployment setup:
- gRPC on localhost:50051
- HTTP on localhost:8007 (not 8080)
- Public URL: muxstr.x.bdw.to
- Metrics enabled with dashboard
- Auth/rate-limiting disabled by default
Update .ship/service to use config file instead of individual flags:
- Old: -db -grpc-addr -ws-addr -public-url flags
- New: -config /var/lib/muxstr/config.yaml
Deployment: Copy config.example.yaml to /var/lib/muxstr/config.yaml
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two critical fixes for metrics:
1. Fix interceptor chaining
- Changed from mixed grpc.UnaryInterceptor/ChainUnaryInterceptor to
proper chaining with grpc.ChainUnaryInterceptor
- Metrics interceptor now runs first (as intended)
- All interceptors properly chained in order: metrics → auth → ratelimit
- This fixes metrics not being recorded for any requests
2. Fix dashboard uptime calculation
- Changed from page load time to process_start_time_seconds metric
- Uptime now persists correctly across page refreshes
- Uses Prometheus standard process_start_time_seconds gauge
Before: Metrics showed 0 for all requests, uptime reset on refresh
After: Metrics properly record all gRPC requests, uptime shows actual relay uptime
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Move metrics dashboard and Prometheus endpoint to the main HTTP server
for simplified deployment and single ingress configuration.
Changes:
- Add PrometheusHandler() and DashboardHandler() methods to Metrics
- Serve /dashboard on main HTTP port (was root on separate port)
- Serve /metrics on main HTTP port (was /metrics on separate port)
- Remove separate metrics server goroutine
- Update logging to show metrics paths on main HTTP port
Benefits:
- Single port/ingress needed for all HTTP traffic
- Simpler reverse proxy configuration
- Dashboard accessible alongside main relay endpoints
Endpoints on port 8080:
- / - WebSocket/index
- /nostr.v1.NostrRelay/* - Connect (gRPC-Web)
- /dashboard - Metrics dashboard (HTML)
- /metrics - Prometheus metrics (text)
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Add a real-time metrics dashboard accessible at the metrics server root.
The dashboard displays relay statistics in a human-readable format with
auto-refresh every 5 seconds.
Features:
- Active connections and subscriptions
- Request counts (total, success, errors)
- Authentication stats (success/failure)
- Rate limiting hits
- Storage metrics (events, DB size, deletions)
- Clean, dark-themed UI with gradient accents
- Auto-refresh and live uptime counter
The dashboard is embedded using go:embed and served at / while
Prometheus metrics continue to be available at /metrics.
Example: http://localhost:9090/ for dashboard
http://localhost:9090/metrics for raw metrics
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Add support for loading configuration from YAML file via -config flag.
Wire up auth, rate limiting, and metrics interceptors based on config.
Changes:
- Add -config flag to relay command
- Use config types directly in auth package (AuthOperationConfig)
- Add conversion methods: RateLimitConfig.ToRateLimiter(), MetricsConfig.ToMetrics()
- Add Metrics.Serve() method for prometheus HTTP endpoint
- Update main.go to initialize interceptors from config
- Fix type naming: OperationAuthConfig -> AuthOperationConfig for consistency
Config now supports complete relay setup including auth read/write
allowlists, rate limiting, and prometheus metrics.
|
| |
|
|
| |
Also removed internal/nostr package - now using northwest.io/nostr library.
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
|
| |
Removed ~100 lines of obvious comments that just repeated what the code does.
Kept only comments that add clarity or valuable detail.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Changed from flat structure to hierarchical read/write config:
Before:
auth:
enabled: bool
required: bool
allowed_npubs_read: []
allowed_npubs_write: []
After:
auth:
read:
enabled: bool
allowed_npubs: []
write:
enabled: bool
allowed_npubs: []
Three states per operation:
- enabled=false: no auth, allow all
- enabled=true, allowed_npubs=[]: auth required, any valid signature
- enabled=true, allowed_npubs=[...]: auth required, whitelist only
Much clearer semantics and easier to reason about.
|
| |
|
|
|
| |
Replace pattern-matching with explicit checks for PublishEvent/PublishBatch.
API is small and well-defined - no need for extensible pattern matching.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
- Split allowed_npubs into allowed_npubs_read and allowed_npubs_write
- Write operations: Publish, Delete, Create, Update, Insert, Remove, Set, Put
- Read operations: everything else (Query, Subscribe, Get, List, etc.)
- Auth interceptor checks appropriate list based on method type
- Enables common patterns:
- Public relay: only some can write, everyone can read
- Private relay: restricted read and write
- Open relay: everyone can read and write
- Updated config, docs, and comprehensive tests
Use cases: "only some can write, everyone can read"
|
| |
|
|
|
|
|
|
|
|
|
| |
- Config now accepts npub format only (human-readable)
- Automatically converts npubs to hex pubkeys at load time
- Updated InterceptorOptions.AllowedPubkeys -> AllowedNpubs
- Added validation to reject hex format in config (npub only)
- Updated documentation to clarify npub-only config
- Added comprehensive tests for npub normalization
Config is for humans (npub), internal code uses hex pubkeys.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Remove misleading max_connections config option and properly configure
SQLite connection pooling in the storage layer.
Changes:
- Set MaxOpenConns(1) for optimal SQLite performance
- Set MaxIdleConns(1) to keep connection alive
- Set ConnMaxLifetime(0) to never close connection
- Remove max_connections and max_lifetime from DatabaseConfig
- Update docs to clarify SQLite's single-writer architecture
Rationale:
SQLite is an embedded database with a single-writer lock. Multiple
connections cause lock contention and reduce performance. WAL mode
allows concurrent reads from the same connection, making connection
pooling unnecessary and counterproductive.
This change makes the configuration clearer and ensures optimal
SQLite performance by using a single long-lived connection.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
## Metrics Package
Comprehensive Prometheus metrics for production observability:
Metrics tracked:
- Request rate, latency, size per method (histograms)
- Active connections and subscriptions (gauges)
- Auth success/failure rates (counters)
- Rate limit hits (counters)
- Storage stats (event count, DB size)
- Standard Go runtime metrics
Features:
- Automatic gRPC instrumentation via interceptors
- Low overhead (~300-500ns per request)
- Standard Prometheus client
- HTTP /metrics endpoint
- Grafana dashboard examples
## Config Package
YAML configuration file support with environment overrides:
Configuration sections:
- Server (addresses, timeouts, public URL)
- Database (path, connections, lifetime)
- Auth (enabled, required, timestamp window, allowed pubkeys)
- Rate limiting (per-method and per-user limits)
- Metrics (endpoint, namespace)
- Logging (level, format, output)
- Storage (compaction, retention)
Features:
- YAML file loading
- Environment variable overrides (MUXSTR_<SECTION>_<KEY>)
- Sensible defaults
- Validation on load
- Duration and list parsing
- Save/export configuration
Both packages include comprehensive README with examples, best
practices, and usage patterns. Config tests verify YAML parsing,
env overrides, validation, and round-trip serialization.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Add comprehensive rate limiting package that works seamlessly with
NIP-98 authentication.
Features:
- Token bucket algorithm (allows bursts, smooth average rate)
- Per-pubkey limits for authenticated users
- Per-IP limits for unauthenticated users (fallback)
- Method-specific overrides (e.g., stricter for PublishEvent)
- Per-user custom limits (VIP/admin tiers)
- Standard gRPC interceptors (chain after auth)
- Automatic cleanup of idle limiters
- Statistics tracking (allowed/denied/denial rate)
Configuration options:
- Default rate limits and burst sizes
- Method-specific overrides
- User-specific overrides (with method overrides)
- Skip methods (health checks, public endpoints)
- Skip users (admins, monitoring)
- Configurable cleanup intervals
Performance:
- In-memory (200 bytes per user)
- O(1) lookups with sync.RWMutex
- ~85ns per rate limit check
- Periodic cleanup to free memory
Returns gRPC ResourceExhausted (HTTP 429) when limits exceeded.
Includes comprehensive tests, benchmarks, and detailed README with
usage examples, configuration reference, and security considerations.
|
| |
|
|
|
|
|
|
|
| |
Explain that the gRPC NIP-98 implementation is effectively NIP-42 for
reads (same pattern: authenticate once, stream many events) and adds
standardized relay access control for writes (beyond event.sig).
Add comparison table showing functional equivalence for streaming reads
and the additional benefits for write access control.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Add comprehensive NIP-98 authentication support following the standard
gRPC credentials.PerRPCCredentials pattern.
Client-side:
- NostrCredentials implements PerRPCCredentials interface
- Automatically signs each request with kind 27235 event
- Drop-in replacement for OAuth2/JWT in gRPC clients
Server-side:
- Unary and stream interceptors for validation
- Extracts and validates NIP-98 events from Authorization headers
- Configurable options (timestamp window, whitelists, skip methods)
- Adds authenticated pubkey to request context
Security features:
- Replay protection via timestamp validation
- Optional payload hash verification
- Signature verification using schnorr
- TLS requirement option
Includes comprehensive test coverage and detailed README with
usage examples and security considerations.
|
| | |
|
| | |
|
| |
|
|
|
| |
Replace ASCII art banner with SVG logo featuring the muxstr branding.
Update page title and background color to match brand identity.
|
| |
|
|
|
|
| |
Update module path from northwest.io/nostr-grpc to northwest.io/muxstr.
This includes updating all Go imports, protobuf definitions, generated
files, and documentation.
|
| |
|
|
|
|
|
|
|
|
| |
WebSocket connections start as GET requests with 'Upgrade: websocket'
header. The handler was serving HTML for ALL GET requests, preventing
WebSocket upgrades from ever happening.
Fix by checking for Upgrade header and only serving HTML/NIP-11 for
non-WebSocket GET requests. Now WebSocket connections return status
101 (Switching Protocols) instead of 200 (OK).
|
| |
|
|
|
|
|
|
|
|
|
| |
Add automatic TLS detection for testclient:
- Use TLS for port 443
- Use TLS for non-localhost addresses
- Use insecure for localhost/127.0.0.1 (development)
Now works with both:
./bin/testclient # local
./bin/testclient -addr nostr-grpc.x.bdw.to:443 # production
|
| |
|
|
|
|
| |
When using --public-url, the gRPC endpoint is accessed via the
reverse proxy on standard HTTPS port 443, not the internal port
50051. Update display to show correct public-facing port.
|
| |
|
|
|
|
|
|
|
|
|
| |
The template was hardcoding 'ws://' prefix, but when using
--public-url we were already passing 'wss://'. This caused
the URL to display as 'ws://wss://domain/'.
Fix by:
- Removing 'ws://' prefix from template
- Always including protocol in the variable (ws:// or wss://)
- Also add http:// prefix for local development consistency
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Add flag to specify public-facing domain when relay is behind
a reverse proxy that terminates TLS.
Usage:
./bin/relay --public-url nostr-grpc.x.bdw.to
When set, the index page displays:
- gRPC: nostr-grpc.x.bdw.to:50051
- Connect: https://nostr-grpc.x.bdw.to
- WebSocket: wss://nostr-grpc.x.bdw.to
When not set, falls back to local addresses (:50051, :8080)
for development environments.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|