Documentation
¶
Overview ¶
Package api hosts the relay-management HTTP handlers. authHelpers is the NIP-98 glue: it pulls a signed kind-27235 event out of the Authorization header, hashes the request body, hands both off to handlers.VerifyNIP98Event, and (for admin endpoints) checks that the authenticated pubkey matches the relay owner in relay_metadata.json.
Per NIP-98 §Encoding the auth header is `Authorization: Nostr <b64>` where <b64> is the base64-encoded JSON of the event.
NIP-86 (Relay Management API) dispatch lives here. The spec — see https://github.com/nostr-protocol/nips/blob/master/86.md — wraps a JSON-RPC-style envelope around a single HTTP endpoint:
POST / HTTP/1.1
Content-Type: application/nostr+json+rpc
Authorization: Nostr <base64-encoded kind-27235 event>
{"method": "<name>", "params": [...]}
The response is `{"result": <method-specific>, "error": "<string>"}`, where `error` is empty on success. Every method requires NIP-98 auth and (in grain's implementation) signer == relay owner per relay_metadata.json — RequireOwner is the gate.
This file ships read-only methods. Mutation methods (banpubkey / allowpubkey / changerelay* / etc.) will land in a follow-up commit once the on-disk write path is in place; `supportedmethods` advertises only what's actually wired so well-behaved clients can feature-detect.
Index ¶
- Constants
- Variables
- func ExtractNIP98Event(r *http.Request) (nostr.Event, error)
- func FetchAdminMutelist(w http.ResponseWriter, r *http.Request)
- func GetAllBlacklistedPubkeys(w http.ResponseWriter, r *http.Request)
- func GetAllBlacklistedPubkeysLive(w http.ResponseWriter, r *http.Request)
- func GetAllWhitelistedPubkeys(w http.ResponseWriter, r *http.Request)
- func GetAllWhitelistedPubkeysLive(w http.ResponseWriter, r *http.Request)
- func GetAuthConfig(w http.ResponseWriter, r *http.Request)
- func GetBackupRelayConfig(w http.ResponseWriter, r *http.Request)
- func GetBlacklistConfig(w http.ResponseWriter, r *http.Request)
- func GetEventPurgeConfig(w http.ResponseWriter, r *http.Request)
- func GetEventTimeConstraintsConfig(w http.ResponseWriter, r *http.Request)
- func GetLoggingConfig(w http.ResponseWriter, r *http.Request)
- func GetRateLimitConfig(w http.ResponseWriter, r *http.Request)
- func GetResourceLimitsConfig(w http.ResponseWriter, r *http.Request)
- func GetServerConfig(w http.ResponseWriter, r *http.Request)
- func GetWhitelistConfig(w http.ResponseWriter, r *http.Request)
- func HandleNIP86(w http.ResponseWriter, r *http.Request)
- func IsRelayOwner(pubkey string) bool
- func RequireOwner(w http.ResponseWriter, r *http.Request) (string, bool)
- func SetServerStatsHook(fn func() ServerStats)
- func SyncAdminMutelist(w http.ResponseWriter, r *http.Request)
- func VerifyAPIAuth(r *http.Request) (string, error)
- type AuthConfigResponse
- type BackupRelayConfigResponse
- type BlacklistConfigResponse
- type BlacklistKeysResponse
- type EventPurgeConfigResponse
- type EventTimeConstraintsConfigResponse
- type LoggingConfigResponse
- type RateLimitConfigResponse
- type ResourceLimitsConfigResponse
- type ServerConfigResponse
- type ServerStats
- type WhitelistConfigResponse
- type WhitelistDomainInfo
- type WhitelistKeysResponse
Constants ¶
const NIP86ContentType = "application/nostr+json+rpc"
NIP86ContentType is the media type NIP-86 reserves. The root dispatcher in server/startup.go uses this string to route requests here ahead of the WebSocket upgrade check.
Variables ¶
var ErrMissingAuthHeader = errors.New("missing Authorization header")
ErrMissingAuthHeader is returned when the Authorization header is not present at all. Callers convert this to a 401 with a WWW-Authenticate prompt so clients know to retry with credentials.
Functions ¶
func ExtractNIP98Event ¶ added in v0.7.0
ExtractNIP98Event pulls the signed event out of an HTTP request's Authorization header. Returns ErrMissingAuthHeader if the header is absent so callers can distinguish "no creds" (401, send challenge) from "bad creds" (401, but no point re-prompting with the same scheme).
func FetchAdminMutelist ¶ added in v0.7.0
func FetchAdminMutelist(w http.ResponseWriter, r *http.Request)
FetchAdminMutelist returns the relay owner's latest kind:10000 + kind:30000(d:"mute") events so the browser can decrypt their private `.content` (#60). The relay does the relay-fetching (outbox resolution + subscription) it already does for public mutelist authors; only decryption has to happen browser-side, where the owner's key lives. GET, owner-gated.
@Summary Fetch owner's mute list events @Description Returns the relay owner's raw NIP-51 mute list events (incl. encrypted content) for browser-side decryption. Owner-only via NIP-98. @Tags relay-admin @Produce json @Success 200 {object} fetchAdminMutelistResponse @Failure 401 {string} string "Unauthorized" @Failure 403 {string} string "Forbidden: signer is not relay owner" @Router /api/v1/relay/admin/mutelist/fetch [get]
func GetAllBlacklistedPubkeys ¶
func GetAllBlacklistedPubkeys(w http.ResponseWriter, r *http.Request)
GetAllBlacklistedPubkeys handles the request to return all blacklisted pubkeys organized by source
@Summary List blacklisted pubkeys (cached) @Description Returns permanent bans (from config), live temporary bans (with expiry timestamps), and the grouped per-author mutelist from the in-process cache. @Tags relay-keys @Produce json @Success 200 {object} BlacklistKeysResponse @Failure 500 {string} string "Blacklist configuration not available" @Router /api/v1/relay/keys/blacklist [get]
func GetAllBlacklistedPubkeysLive ¶
func GetAllBlacklistedPubkeysLive(w http.ResponseWriter, r *http.Request)
GetAllBlacklistedPubkeysLive handles the request to return all blacklisted pubkeys with live mutelist fetching
@Summary List blacklisted pubkeys (live) @Description Same shape as the cached endpoint but bypasses the mutelist cache to fetch each author's mutelist from their outbox relays. Slower but always fresh. @Tags relay-keys @Produce json @Success 200 {object} BlacklistKeysResponse @Failure 500 {string} string "Blacklist configuration not available" @Router /api/v1/relay/keys/blacklist/live [get]
func GetAllWhitelistedPubkeys ¶
func GetAllWhitelistedPubkeys(w http.ResponseWriter, r *http.Request)
GetAllWhitelistedPubkeys handles the cached request to return all whitelisted pubkeys organized by source
@Summary List whitelisted pubkeys (cached) @Description Returns the configured whitelist registry (direct pubkeys + npubs converted) and a per-domain breakdown from the in-process cache. This is the registry, not the enforcement gate — values are returned regardless of whether the whitelist is currently enabled. See `/api/v1/relay/keys/whitelist/live` for a fresh fetch. @Tags relay-keys @Produce json @Success 200 {object} WhitelistKeysResponse @Failure 500 {string} string "Whitelist configuration not available" @Router /api/v1/relay/keys/whitelist [get]
func GetAllWhitelistedPubkeysLive ¶
func GetAllWhitelistedPubkeysLive(w http.ResponseWriter, r *http.Request)
GetAllWhitelistedPubkeysLive handles the request to return all whitelisted pubkeys with live domain fetching This endpoint fetches fresh data from domains and is suitable for verification after configuration changes
@Summary List whitelisted pubkeys (live) @Description Same shape as the cached endpoint but fetches every `.well-known/nostr.json` referenced by `domain_whitelist` on demand. Useful after a config change when you want to verify domain resolution without waiting for the background refresh. @Tags relay-keys @Produce json @Success 200 {object} WhitelistKeysResponse @Failure 500 {string} string "Whitelist configuration not available" @Router /api/v1/relay/keys/whitelist/live [get]
func GetAuthConfig ¶
func GetAuthConfig(w http.ResponseWriter, r *http.Request)
GetAuthConfig handles the request to return authentication configuration
@Summary Get auth config @Description Returns whether NIP-42 AUTH is required on websocket connections and the relay URL clients must echo back in their AUTH events. @Tags relay-config @Produce json @Success 200 {object} AuthConfigResponse @Failure 500 {string} string "Server configuration not available" @Router /api/v1/relay/config/auth [get]
func GetBackupRelayConfig ¶
func GetBackupRelayConfig(w http.ResponseWriter, r *http.Request)
GetBackupRelayConfig handles the request to return backup relay configuration
@Summary Get backup relay config @Description Returns the upstream relay grain mirrors accepted events to (URL plus enabled flag). @Tags relay-config @Produce json @Success 200 {object} BackupRelayConfigResponse @Failure 500 {string} string "Server configuration not available" @Router /api/v1/relay/config/backup_relay [get]
func GetBlacklistConfig ¶
func GetBlacklistConfig(w http.ResponseWriter, r *http.Request)
GetBlacklistConfig handles the request to return blacklist configuration
@Summary Get blacklist config @Description Returns the full blacklist.yml — permanent and temporary ban words, blacklisted pubkeys/npubs, and the mutelist authors whose mutelists grain consumes. @Tags relay-config @Produce json @Success 200 {object} BlacklistConfigResponse @Failure 500 {string} string "Blacklist configuration not available" @Router /api/v1/relay/config/blacklist [get]
func GetEventPurgeConfig ¶
func GetEventPurgeConfig(w http.ResponseWriter, r *http.Request)
GetEventPurgeConfig handles the request to return event purging configuration
@Summary Get event-purge config @Description Returns retention settings — what's purged, by kind/category, on what interval, and whether whitelisted authors are exempt. @Tags relay-config @Produce json @Success 200 {object} EventPurgeConfigResponse @Failure 500 {string} string "Server configuration not available" @Router /api/v1/relay/config/event_purge [get]
func GetEventTimeConstraintsConfig ¶
func GetEventTimeConstraintsConfig(w http.ResponseWriter, r *http.Request)
GetEventTimeConstraintsConfig handles the request to return event time constraints configuration
@Summary Get event-time-constraints config @Description Returns the accepted window for event `created_at` (min/max as Unix timestamps and human-readable strings). @Tags relay-config @Produce json @Success 200 {object} EventTimeConstraintsConfigResponse @Failure 500 {string} string "Server configuration not available" @Router /api/v1/relay/config/event_time_constraints [get]
func GetLoggingConfig ¶
func GetLoggingConfig(w http.ResponseWriter, r *http.Request)
GetLoggingConfig handles the request to return logging configuration
@Summary Get logging config @Description Returns log level, file path, rotation settings, and per-component suppressions. @Tags relay-config @Produce json @Success 200 {object} LoggingConfigResponse @Failure 500 {string} string "Server configuration not available" @Router /api/v1/relay/config/logging [get]
func GetRateLimitConfig ¶
func GetRateLimitConfig(w http.ResponseWriter, r *http.Request)
GetRateLimitConfig handles the request to return rate limiting configuration
@Summary Get rate limit config @Description Returns the rate-limit settings (ws/event/req limits and bursts, kind/category overrides, max event size). @Tags relay-config @Produce json @Success 200 {object} RateLimitConfigResponse @Failure 500 {string} string "Server configuration not available" @Router /api/v1/relay/config/rate_limit [get]
func GetResourceLimitsConfig ¶
func GetResourceLimitsConfig(w http.ResponseWriter, r *http.Request)
GetResourceLimitsConfig handles the request to return resource limits configuration
@Summary Get resource limits config @Description Returns the OS resource caps the relay self-imposes — CPU cores, memory ceiling, Go heap target. @Tags relay-config @Produce json @Success 200 {object} ResourceLimitsConfigResponse @Failure 500 {string} string "Server configuration not available" @Router /api/v1/relay/config/resource_limits [get]
func GetServerConfig ¶
func GetServerConfig(w http.ResponseWriter, r *http.Request)
GetServerConfig handles the request to return server configuration
@Summary Get server config @Description Returns the HTTP server settings (timeouts, connection caps, subscription caps). @Tags relay-config @Produce json @Success 200 {object} ServerConfigResponse @Failure 500 {string} string "Server configuration not available" @Router /api/v1/relay/config/server [get]
func GetWhitelistConfig ¶
func GetWhitelistConfig(w http.ResponseWriter, r *http.Request)
GetWhitelistConfig handles the request to return whitelist configuration
@Summary Get whitelist config @Description Returns the full whitelist.yml — pubkey, kind, and domain whitelists with their enabled flags. The configured set is grain's "elevated users" registry; enforcement only happens when `enabled` is true. @Tags relay-config @Produce json @Success 200 {object} WhitelistConfigResponse @Failure 500 {string} string "Whitelist configuration not available" @Router /api/v1/relay/config/whitelist [get]
func HandleNIP86 ¶ added in v0.7.0
func HandleNIP86(w http.ResponseWriter, r *http.Request)
HandleNIP86 is the JSON-RPC entry point. All paths return HTTP 200 with a JSON body — method errors live in the response envelope, not the HTTP status. Auth failures (no header, bad signature, non-owner) short-circuit via RequireOwner before any JSON-RPC parsing happens and DO use the right status codes (401 / 403) because they're not JSON-RPC errors — they're HTTP-level access control.
@Summary NIP-86 relay management @Description JSON-RPC over a single POST endpoint per [NIP-86](https://github.com/nostr-protocol/nips/blob/master/86.md). Requires NIP-98 HTTP Auth (kind:27235 with `u`, `method`, `payload` tags) and the signer must equal the relay owner pubkey in `relay_metadata.json`. Body is `{"method": "<name>", "params": [...]}`; response is `{"result": ..., "error": ""}`. Errors live in the envelope, not the HTTP status — only auth failures return 401/403. @Description @Description **Spec methods (reads):** `supportedmethods`, `listallowedpubkeys`, `listbannedpubkeys`, `listallowedkinds`, `listblockedips`. @Description @Description **Spec methods (writes):** `banpubkey` / `unbanpubkey` / `allowpubkey` / `unallowpubkey` (params: `[pubkey, reason?]`), `allowkind` / `disallowkind` (params: `[kind:int]`), `blockip` / `unblockip` (params: `[ip-or-cidr, reason?]`), `changerelayname` / `changerelaydescription` / `changerelayicon` (params: `[value:string]`). @Description @Description **Grain vendor extensions (writes):** `grain_updateserver`, `grain_updateratelimit`, `grain_updateeventpurge`, `grain_updatelogging`, `grain_updateauth`, `grain_updatebackuprelay`, `grain_updateresourcelimits`, `grain_updateeventtimeconstraints`, `grain_updatewhitelistconfig`, `grain_updateblacklistconfig`. Each takes the full section blob as `params[0]` (same shape the matching GET endpoint returns) and stages it to disk; the response is `{ok:true, restart_pending:true}`. Operator clicks Apply → dashboard calls `grain_reloadconfig`. @Description @Description **Grain vendor extensions (ops + reads):** `grain_reloadconfig` (triggers restart), `grain_refreshcache` (synchronous whitelist + blacklist cache refresh), `grain_whitelistconfig` / `grain_blacklistconfig` (full-struct reads — the blacklist read overlays IP fields from config.yml so the dashboard sees one coherent shape), `grain_stats_overview` (server counters + list/cache stats). @Description @Description **Out of scope:** event-moderation methods (`allowevent` / `banevent` / `listbannedevents` / `listeventsneedingmoderation`) need a moderation queue that doesn't exist yet — tracked separately. Call `supportedmethods` at runtime for the authoritative list this build advertises. @Tags nip86 @Accept json @Produce json @Param body body nip86Request true "JSON-RPC envelope" @Success 200 {object} nip86Response @Failure 401 {string} string "Unauthorized — missing/bad NIP-98 header" @Failure 403 {string} string "Forbidden — signer is not relay owner" @Security NostrAuth @Router / [post]
func IsRelayOwner ¶ added in v0.7.0
IsRelayOwner reports whether the given pubkey is the relay owner recorded in relay_metadata.json. The comparison is case-insensitive because some clients/tools normalize hex differently; the on-disk metadata is the source of truth.
func RequireOwner ¶ added in v0.7.0
RequireOwner is the gate for admin/management endpoints: it authenticates the request via NIP-98 and confirms the signer is the relay owner. On failure it writes the appropriate response and returns ok=false so the handler can simply early-return.
func SetServerStatsHook ¶ added in v0.7.0
func SetServerStatsHook(fn func() ServerStats)
SetServerStatsHook is wired from server/startup.go once the counters are available. The hook is called per grain_stats_overview request — should be cheap (atomic loads).
func SyncAdminMutelist ¶ added in v0.7.0
func SyncAdminMutelist(w http.ResponseWriter, r *http.Request)
SyncAdminMutelist accepts the relay owner's decrypted private mute list and folds it into the blacklist (#60).
The relay can't decrypt NIP-51 `.content` — it has no private key — so decryption happens in the owner's browser and only the resulting plain pubkey list reaches this endpoint. POST-only, gated by RequireOwner (NIP-98 + relay-owner check). An empty pubkey list clears the owner's contribution (un-sync).
@Summary Sync owner's private mute list @Description Stores the relay owner's decrypted NIP-51 private mute pubkeys as a blacklist source. Owner-only via NIP-98. @Tags relay-admin @Accept json @Produce json @Param body body syncAdminMutelistRequest true "Decrypted pubkey set" @Success 200 {object} syncAdminMutelistResponse @Failure 401 {string} string "Unauthorized" @Failure 403 {string} string "Forbidden: signer is not relay owner" @Router /api/v1/relay/admin/mutelist/sync [post]
func VerifyAPIAuth ¶ added in v0.7.0
VerifyAPIAuth verifies the NIP-98 Authorization header on r and returns the authenticated pubkey. It reads the request body (if any) to compute the payload hash that NIP-98 requires; the body is restored on r.Body so downstream handlers can read it normally.
Callers that need owner-only access should use RequireOwner, which wraps this with a relay_metadata.json owner check.
Types ¶
type AuthConfigResponse ¶
type AuthConfigResponse struct {
Required bool `json:"required"`
RelayURL string `json:"relay_url"`
}
AuthConfigResponse represents the authentication configuration response
type BackupRelayConfigResponse ¶
BackupRelayConfigResponse represents the backup relay configuration response
type BlacklistConfigResponse ¶
type BlacklistConfigResponse struct {
Enabled bool `json:"enabled"`
PermanentBanWords []string `json:"permanent_ban_words"`
TempBanWords []string `json:"temp_ban_words"`
MaxTempBans int `json:"max_temp_bans"`
TempBanDuration int `json:"temp_ban_duration"`
PermanentBlacklistPubkeys []string `json:"permanent_blacklist_pubkeys"`
PermanentBlacklistNpubs []string `json:"permanent_blacklist_npubs"`
MuteListAuthors []string `json:"mutelist_authors"`
MutelistCacheRefreshMinutes int `json:"mutelist_cache_refresh_minutes"`
}
BlacklistConfigResponse represents the blacklist configuration response
type BlacklistKeysResponse ¶
type BlacklistKeysResponse struct {
Permanent []string `json:"permanent"`
Temporary []map[string]interface{} `json:"temporary"`
Mutelist map[string][]string `json:"mutelist"`
AdminMutelistCount int `json:"admin_mutelist_count"`
}
BlacklistKeysResponse represents the blacklist keys response.
AdminMutelistCount is a privacy-preserving COUNT, never the pubkeys: the relay owner's private mute list (#60) is decrypted from their own NIP-51 content, and this endpoint is public + rendered on the public home page. Listing those pubkeys would leak who the owner privately muted, so we only surface how many were folded into the blacklist.
type EventPurgeConfigResponse ¶
type EventPurgeConfigResponse struct {
Enabled bool `json:"enabled"`
DisableAtStartup bool `json:"disable_at_startup"`
KeepIntervalHours int `json:"keep_interval_hours"`
PurgeIntervalMinutes int `json:"purge_interval_minutes"`
PurgeByCategory map[string]bool `json:"purge_by_category"`
PurgeByKindEnabled bool `json:"purge_by_kind_enabled"`
KindsToPurge []int `json:"kinds_to_purge"`
ExcludeWhitelisted bool `json:"exclude_whitelisted"`
}
EventPurgeConfigResponse represents the event purging configuration response
type EventTimeConstraintsConfigResponse ¶
type EventTimeConstraintsConfigResponse struct {
MinCreatedAt int64 `json:"min_created_at"`
MinCreatedAtString string `json:"min_created_at_string"`
MaxCreatedAt int64 `json:"max_created_at"`
MaxCreatedAtString string `json:"max_created_at_string"`
}
EventTimeConstraintsConfigResponse represents the event time constraints configuration response
type LoggingConfigResponse ¶
type LoggingConfigResponse struct {
Level string `json:"level"`
File string `json:"file"`
MaxSizeMB int `json:"max_log_size_mb"`
Structure bool `json:"structure"`
CheckIntervalMin int `json:"check_interval_min"`
BackupCount int `json:"backup_count"`
SuppressComponents []string `json:"suppress_components"`
}
LoggingConfigResponse represents the logging configuration response
type RateLimitConfigResponse ¶
type RateLimitConfigResponse struct {
WsLimit float64 `json:"ws_limit"`
WsBurst int `json:"ws_burst"`
EventLimit float64 `json:"event_limit"`
EventBurst int `json:"event_burst"`
ReqLimit float64 `json:"req_limit"`
ReqBurst int `json:"req_burst"`
MaxEventSize int `json:"max_event_size"`
KindSizeLimits []cfgType.KindSizeLimitConfig `json:"kind_size_limits"`
CategoryLimits map[string]cfgType.KindLimitConfig `json:"category_limits"`
KindLimits []cfgType.KindLimitConfig `json:"kind_limits"`
}
RateLimitConfigResponse represents the rate limiting configuration response
type ResourceLimitsConfigResponse ¶
type ResourceLimitsConfigResponse struct {
CPUCores int `json:"cpu_cores"`
MemoryMB int `json:"memory_mb"`
HeapSizeMB int `json:"heap_size_mb"`
}
ResourceLimitsConfigResponse represents the resource limits configuration response
type ServerConfigResponse ¶
type ServerConfigResponse struct {
ReadTimeout int `json:"read_timeout"`
WriteTimeout int `json:"write_timeout"`
IdleTimeout int `json:"idle_timeout"`
MaxConnections int `json:"max_connections"`
MaxSubscriptionsPerClient int `json:"max_subscriptions_per_client"`
ImplicitReqLimit int `json:"implicit_req_limit"`
}
ServerConfigResponse represents the server configuration response
type ServerStats ¶ added in v0.7.0
type ServerStats struct {
ActiveConnections int64 `json:"active_connections"`
TotalMessagesSent int64 `json:"total_messages_sent"`
UptimeSeconds int64 `json:"uptime_seconds"`
Version string `json:"version"`
BuildTime string `json:"build_time"`
GitCommit string `json:"git_commit"`
}
ServerStats is the slice of stats only the server package can produce. Kept small on purpose — every field added here has to thread through the hook.
type WhitelistConfigResponse ¶
type WhitelistConfigResponse struct {
PubkeyWhitelist struct {
Enabled bool `json:"enabled"`
Pubkeys []string `json:"pubkeys"`
Npubs []string `json:"npubs"`
CacheRefreshMinutes int `json:"cache_refresh_minutes"`
} `json:"pubkey_whitelist"`
KindWhitelist struct {
Enabled bool `json:"enabled"`
Kinds []string `json:"kinds"`
} `json:"kind_whitelist"`
DomainWhitelist struct {
Enabled bool `json:"enabled"`
Domains []string `json:"domains"`
CacheRefreshMinutes int `json:"cache_refresh_minutes"`
} `json:"domain_whitelist"`
}
WhitelistConfigResponse represents the whitelist configuration response
type WhitelistDomainInfo ¶
WhitelistDomainInfo represents domain information with its pubkeys
type WhitelistKeysResponse ¶
type WhitelistKeysResponse struct {
List []string `json:"list"`
Domains []WhitelistDomainInfo `json:"domains"`
}
WhitelistKeysResponse represents the whitelist keys response
Source Files
¶
- adminMutelistSync.go
- authConfig.go
- authHelpers.go
- backupRelayConfig.go
- blacklistConfig.go
- blacklistKeys.go
- blacklistKeysLive.go
- eventPurgeConfig.go
- eventTimeConstraintsConfig.go
- loggingConfig.go
- nip86.go
- nip86_grain.go
- nip86_writes.go
- rateLimitConfig.go
- resourceLimitsConfig.go
- serverConfig.go
- stats.go
- whitelistConfig.go
- whitelistKeys.go
- whitelistKeysLive.go