mocrelay

package module
v0.18.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 3, 2026 License: MIT Imports: 34 Imported by: 0

README

mocrelay

A middleware-composable Nostr relay library for Go.

Go Reference

Features

  • Middleware Architecture - Compose handlers and middleware to build your relay
  • NIP-11 Driven - Built-in middleware maps directly to NIP-11 limitation fields
  • Pure Go - No cgo dependencies, single binary deployment
  • Persistent Storage - Pebble-based LSM-tree storage with MVCC support
  • Full-text Search - Bleve-based NIP-50 search with CJK support
  • Prometheus Metrics - Built-in instrumentation for relay, router, auth, and storage
  • Standard http.Handler - Composes naturally with net/http.ServeMux

Requirements

  • Go 1.25 or later
  • GOEXPERIMENT=jsonv2 environment variable
export GOEXPERIMENT=jsonv2

Installation

go get github.com/high-moctane/mocrelay

Development

Git hooks are optional. To run go fix and gofmt automatically on staged Go files before each commit, install lefthook and run:

lefthook install

The lefthook.yml in the repository configures the hooks. CI runs the same checks, so skipping local hooks will not bypass them.

Quick Start

The simplest possible relay is a single line:

func main() {
	log.Fatal(http.ListenAndServe(":7447", mocrelay.NewRelay(mocrelay.NewNopHandler(), nil)))
}

This accepts all events and returns EOSE for all subscriptions — a valid relay with zero storage, in one line.

For a more practical starting point, see the examples.

Examples

The cmd/examples/ directory provides three graduated examples:

Example Description
nop One-line relay. Demonstrates that a Handler is all you need.
minimal In-memory storage, real-time routing, and basic middleware. A functional relay in ~80 lines.
kitchen-sink Pebble + Bleve + all middleware + Prometheus + ServeMux. Everything mocrelay offers.

Architecture

            ┌──────────────────────────────────────────────┐
            │          Relay (http.Handler)                │
            │          WebSocket lifecycle management      │
            ├──────────────────────────────────────────────┤
            │          Middleware Pipeline                 │
            │   Auth → ProtectedEvents → Limits → ...      │
            ├──────────────────────────────────────────────┤
            │              MergeHandler                    │
            │     ┌────────────────┬──────────────────┐    │
            │     │ StorageHandler │  RouterHandler   │    │
            │     │ (past events)  │  (real-time)     │    │
            │     └───────┬────────┴────────┬─────────┘    │
            ├─────────────┼─────────────────┼──────────────┤
            │   ┌─────────┴────────┐  ┌─────┴───────┐      │
            │   │ CompositeStorage │  │   Router    │      │
            │   │  ┌──────┬──────┐ │  │ (broadcast) │      │
            │   │  │Pebble│Bleve │ │  └─────────────┘      │
            │   │  └──────┴──────┘ │                       │
            │   └──────────────────┘                       │
            └──────────────────────────────────────────────┘
  • Relay serves HTTP/WebSocket and manages connection lifecycles
  • Handler processes messages for a single connection
  • Middleware wraps a Handler to add cross-cutting concerns
  • Storage persists and queries events using Go iterators (iter.Seq)
  • Router routes events between connected clients in real-time

Available Middleware

Middleware NIP-11 Field Description
MaxSubscriptions max_subscriptions Limit subscriptions per connection
MaxSubidLength max_subid_length Limit subscription ID length
MaxLimit max_limit, default_limit Clamp filter limit values
MaxEventTags max_event_tags Limit tags per event
MaxContentLength max_content_length Limit content length
CreatedAtLimits created_at_lower/upper_limit Restrict event timestamps
KindDenylist retention Block specific event kinds
RestrictedWrites restricted_writes Pubkey allow/deny list
MinPowDifficulty min_pow_difficulty Require proof-of-work (NIP-13)
Auth auth_required Require authentication (NIP-42)
Expiration - Handle event expiration (NIP-40)
ProtectedEvents - Protect events from republishing (NIP-70)

Multiple middleware are composed into a single pipeline via NewSimpleMiddleware:

handler = mocrelay.NewSimpleMiddleware(
    mocrelay.NewMaxSubscriptionsMiddlewareBase(20),
    mocrelay.NewMaxLimitMiddlewareBase(500, 100),
    mocrelay.NewKindDenylistMiddlewareBase([]int64{4, 1059}),
)(handler)

NIP Support

NIP Description
NIP-01 Basic Protocol
NIP-09 Event Deletion
NIP-11 Relay Information
NIP-13 Proof of Work
NIP-40 Expiration Timestamp
NIP-42 Authentication
NIP-45 Event Counts
NIP-50 Search Capability
NIP-70 Protected Events

License

MIT

Documentation

Overview

Package mocrelay implements a Nostr relay as a composable Go library.

mocrelay provides a middleware-composable architecture for building Nostr relays. The core abstraction is the Handler interface, which processes a single WebSocket connection's lifetime. Handlers can be composed using Middleware to add features like authentication, rate limiting, and content filtering.

Architecture

The key types form a layered architecture:

  • Relay serves HTTP/WebSocket and manages connection lifecycles
  • Handler processes messages for a single connection
  • Middleware wraps a Handler to add cross-cutting concerns
  • Storage persists and queries events
  • Router routes events between connected clients in real-time

Handlers

mocrelay provides several built-in handlers:

Most handlers can be implemented using SimpleHandlerBase, which provides a simpler message-at-a-time interface instead of managing channels directly.

Middleware

Middleware is built on the SimpleMiddlewareBase interface. Multiple middleware bases are composed into a single pipeline via NewSimpleMiddleware:

handler := NewSimpleMiddleware(
    NewMaxSubscriptionsMiddlewareBase(20),
    NewMaxLimitMiddlewareBase(500, 100),
    NewKindDenylistMiddlewareBase([]int64{4, 1059}),
)(innerHandler)

Built-in middleware corresponds to NIP-11 limitation and retention fields, providing a declarative way to configure relay policies.

Storage

The Storage interface uses Go iterators (iter.Seq) for streaming query results:

events, errFn, closeFn := storage.Query(ctx, filters)
defer closeFn()
for event := range events {
    // process event
}
if err := errFn(); err != nil {
    // handle error
}

InMemoryStorage is provided for testing. For production use, see PebbleStorage which wraps a caller-owned CockroachDB Pebble LSM-tree *pebble.DB. Full-text search (NIP-50) is available via BleveIndex, which wraps a caller-owned bleve.Index.

Typical usage

A typical relay combines storage, routing, middleware, and metrics. The caller owns the underlying pebble.DB (and bleve.Index, if used), which keeps pebble.DB.Metrics and database lifecycle in the caller's hands:

db, _ := pebble.Open("/path/to/db", nil)
defer db.Close()
storage := NewPebbleStorage(db, nil)

router := NewRouter(nil)
handler := NewMergeHandler(
    []Handler{
        NewStorageHandler(storage, nil),
        NewRouterHandler(router),
    },
    nil,
)

handler = NewSimpleMiddleware(
    NewMaxSubscriptionsMiddlewareBase(20),
    NewMaxLimitMiddlewareBase(500, 100),
)(handler)

relay := NewRelay(handler, nil)
http.ListenAndServe(":7447", relay)

Index

Constants

View Source
const (
	// Client-to-relay message types.
	MsgTypeEvent = "EVENT" // Submit an event
	MsgTypeReq   = "REQ"   // Subscribe with filters
	MsgTypeClose = "CLOSE" // Unsubscribe
	MsgTypeAuth  = "AUTH"  // NIP-42 authentication
	MsgTypeCount = "COUNT" // NIP-45 event counting

	// Relay-to-client message types.
	MsgTypeOK     = "OK"     // Event submission result
	MsgTypeEOSE   = "EOSE"   // End of stored events
	MsgTypeClosed = "CLOSED" // Subscription closed by relay
	MsgTypeNotice = "NOTICE" // Human-readable message
)

Nostr message type labels as defined in NIP-01.

View Source
const DefaultMergeHandlerBroadcastTimeout = 30 * time.Second

DefaultMergeHandlerBroadcastTimeout is the default per-child broadcast timeout used by the merge handler when [MergeHandlerOptions.BroadcastTimeout] is zero.

View Source
const DefaultStorageHandlerSlowQueryThreshold = 1 * time.Second

DefaultStorageHandlerSlowQueryThreshold is the default threshold for logging a slow REQ / COUNT subscription when [StorageHandlerOptions.SlowQueryThreshold] is zero.

View Source
const MergeHandlerOKLostHandlerMessage = "error: a downstream handler is unavailable, please retry"

MergeHandlerOKLostHandlerMessage is the OK message text returned to the client when the merge handler cannot account for every downstream handler's verdict on an EVENT — typically because a child timed out on broadcast or exited before responding. The OK is forced to accepted=false so a well-behaved client retries the event, which can then be re-fanned to any downstream that has since recovered.

Variables

This section is empty.

Functions

func BuildIndexMapping added in v0.6.0

func BuildIndexMapping() *mapping.IndexMappingImpl

BuildIndexMapping returns the Bleve mapping.IndexMappingImpl that BleveIndex expects. Use it when opening a bleve.Index to pass to NewBleveIndex:

idx, _ := bleve.NewMemOnly(mocrelay.BuildIndexMapping())
searchIndex := mocrelay.NewBleveIndex(idx, nil)

It configures:

  • "content" as a text field analyzed with the CJK analyzer (bigram tokenization, covers Japanese / Chinese / Korean).
  • "id" as stored-only (indexed=false) so hits can be mapped back to event IDs without bloating the index.
  • "pubkey" as a keyword (exact match) field.
  • "kind" and "created_at" as numeric fields.

Callers who need a customized mapping can build on top of the returned value, but Index / Search / Delete rely on the document shape above.

func ConnIDFromContext

func ConnIDFromContext(ctx context.Context) string

ConnIDFromContext returns the connection ID from the context. If no connection ID is found, it returns an empty string.

func ContextWithLogger

func ContextWithLogger(ctx context.Context, logger *slog.Logger) context.Context

ContextWithLogger returns a new context with the given logger.

func LoggerFromContext

func LoggerFromContext(ctx context.Context) *slog.Logger

LoggerFromContext returns the logger from the context. If no logger is found, it returns slog.Default().

Types

type AuthMiddlewareOptions

type AuthMiddlewareOptions struct {
	// CreatedAtTolerance is the maximum age of auth events.
	// Default: 10 minutes (as recommended by NIP-42)
	CreatedAtTolerance time.Duration
}

AuthMiddlewareOptions configures the middleware returned by NewAuthMiddlewareBase. All fields are optional; the zero value gives sensible defaults.

Metrics are not part of this struct. Auth metrics are constructed by Relay from [RelayOptions.Registerer] and injected into the request context; the auth middleware reads them internally without holding a direct reference, mirroring the rejection counter / Logger pattern.

type BleveIndex

type BleveIndex struct {
	// contains filtered or unexported fields
}

BleveIndex implements SearchIndex using Bleve with CJK analyzer.

The bleve.Index is owned by the caller: BleveIndex does not open or close it. Callers should create the index with BuildIndexMapping (or a customized mapping built on top of it) so the document schema matches what Index / Search / Delete expect, then close the underlying index when done. BleveIndex itself has no Close method.

func NewBleveIndex

func NewBleveIndex(idx bleve.Index, opts *BleveIndexOptions) *BleveIndex

NewBleveIndex wraps an already-open bleve.Index as a SearchIndex.

The caller retains ownership of idx: they are responsible for opening it (typically with BuildIndexMapping) and for closing it when done. BleveIndex itself has no Close method.

opts can be nil; there are currently no configurable options.

func (*BleveIndex) Delete

func (b *BleveIndex) Delete(ctx context.Context, eventID string) error

Delete implements SearchIndex.Delete.

func (*BleveIndex) Index

func (b *BleveIndex) Index(ctx context.Context, event *Event) error

Index implements SearchIndex.Index.

func (*BleveIndex) Search

func (b *BleveIndex) Search(ctx context.Context, query string, limit int64) ([]string, error)

Search implements SearchIndex.Search.

type BleveIndexOptions

type BleveIndexOptions struct{}

BleveIndexOptions is reserved for future BleveIndex configuration. It is currently empty but exists so that adding options later is not a breaking API change. Pass nil for now.

type ClientMsg

type ClientMsg struct {
	Type string

	// EVENT, AUTH: the event being submitted
	Event *Event

	// REQ, CLOSE, COUNT: subscription identifier
	SubscriptionID string

	// REQ, COUNT: filters for the subscription
	Filters []*ReqFilter
}

ClientMsg represents a message from client to relay. This is a union type - check Type field to determine which fields are valid.

func ParseClientMsg

func ParseClientMsg(data []byte) (*ClientMsg, error)

ParseClientMsg parses a JSON message from client.

type CompositeStorage

type CompositeStorage struct {
	// contains filtered or unexported fields
}

CompositeStorage combines a primary Storage with a SearchIndex. The primary storage (e.g., PebbleStorage) is the source of truth. The search index (e.g., BleveIndex) provides full-text search capabilities.

Query behavior:

  • If filter has Search field: use SearchIndex to get event IDs, then fetch from primary
  • Otherwise: use primary storage directly

Store behavior:

  • Store in primary first (source of truth)
  • If successful, index in search index (best effort)

Both the search path and the index path swallow their backend's errors from the caller's perspective (search falls back to primary; index is fire-and-forget). Wire [CompositeStorageOptions.Metrics] to make those failures visible via [CompositeStorageMetrics.SearchErrors] / [CompositeStorageMetrics.IndexErrors]; a structured Warn log is emitted alongside each failure via LoggerFromContext.

func NewCompositeStorage

func NewCompositeStorage(primary Storage, search SearchIndex, opts *CompositeStorageOptions) *CompositeStorage

NewCompositeStorage creates a new CompositeStorage. primary is the source of truth for all data. search provides full-text search capabilities (can be nil to disable search). opts can be nil; see CompositeStorageOptions for configurable options.

func (*CompositeStorage) Query

func (s *CompositeStorage) Query(ctx context.Context, filters []*ReqFilter) (iter.Seq[*Event], func() error, func() error)

Query implements Storage.Query. If filter contains Search field, uses search index. Otherwise, uses primary.

func (*CompositeStorage) Store

func (s *CompositeStorage) Store(ctx context.Context, event *Event) (bool, error)

Store implements Storage.Store. Stores in primary first, then indexes in search (if available).

type CompositeStorageOptions added in v0.7.0

type CompositeStorageOptions struct {
	// Registerer is the Prometheus registry that CompositeStorage's
	// internal search / indexing counters are registered with
	// (mocrelay_search_total, mocrelay_search_errors_total,
	// mocrelay_index_total, mocrelay_index_errors_total). If nil, no
	// metrics are collected (zero runtime cost at each instrument site).
	//
	// The underlying Bleve / SearchIndex resource state itself (doc count,
	// index size, batch statistics) lives on the caller's [bleve.Index]
	// and should be collected directly from idx.StatsMap() by the caller.
	Registerer prometheus.Registerer
}

CompositeStorageOptions configures CompositeStorage. Pass nil for defaults.

type Event

type Event struct {
	ID        string    `json:"id"`
	Pubkey    string    `json:"pubkey"`
	CreatedAt time.Time `json:"created_at,format:unix"`
	Kind      int64     `json:"kind"`
	Tags      []Tag     `json:"tags"`
	Content   string    `json:"content"`
	Sig       string    `json:"sig"`
}

Event represents a Nostr event as defined in NIP-01.

func (*Event) Address

func (e *Event) Address() string

Address returns the address for replaceable/addressable events. Format: "kind:pubkey:" for replaceable, "kind:pubkey:d-tag" for addressable. Returns empty string for regular/ephemeral events.

func (*Event) EventType

func (e *Event) EventType() EventType

EventType returns the type of the event based on its kind.

func (Event) MarshalJSONTo

func (e Event) MarshalJSONTo(enc *jsontext.Encoder) error

MarshalJSONTo implements json.MarshalerTo to ensure NIP-01 compliant output. Tags is always marshaled as [] (never null).

func (*Event) Serialize

func (e *Event) Serialize() ([]byte, error)

Serialize returns the canonical JSON representation for signing/verification. Format: [0, pubkey, created_at, kind, tags, content]

func (*Event) UnmarshalJSONFrom

func (e *Event) UnmarshalJSONFrom(dec *jsontext.Decoder) error

UnmarshalJSONFrom implements json.UnmarshalerFrom to validate field count and field value types. Nostr events must have exactly 7 fields, all non-null.

func (*Event) Valid

func (e *Event) Valid() bool

Valid checks if the event has valid format (not cryptographic validity).

func (*Event) Verify

func (e *Event) Verify() (bool, error)

Verify checks if the event ID and signature are cryptographically valid.

type EventType

type EventType int

EventType represents the category of a Nostr event based on its kind.

const (
	// EventTypeRegular represents standard events (kind 1, 2, 4-44, 1000-9999).
	// All regular events are stored.
	EventTypeRegular EventType = iota

	// EventTypeReplaceable represents events where only the latest is kept per (pubkey, kind).
	// Kinds: 0, 3, 10000-19999.
	EventTypeReplaceable

	// EventTypeEphemeral represents events that are not stored.
	// Kinds: 20000-29999.
	EventTypeEphemeral

	// EventTypeAddressable represents events where only the latest is kept per (pubkey, kind, d-tag).
	// Kinds: 30000-39999.
	EventTypeAddressable
)

type Handler

type Handler interface {
	ServeNostr(ctx context.Context, send chan<- *ServerMsg, recv <-chan *ClientMsg) error
}

Handler is the interface for processing Nostr client connections. It receives client messages and sends server messages through channels.

The handler runs for the lifetime of a single WebSocket connection. When the handler returns:

  • error == nil: normal termination (client disconnected gracefully)
  • error != nil: abnormal termination (connection will be closed, error logged)

The decision to shut down the entire relay is NOT the handler's responsibility. That should be handled at the HTTP handler level.

func NewMergeHandler

func NewMergeHandler(handlers []Handler, opts *MergeHandlerOptions) Handler

NewMergeHandler returns a Handler that fans messages out to every handler in handlers and merges their responses. Pass nil for opts to use defaults.

func NewNopHandler

func NewNopHandler() Handler

NewNopHandler returns a Handler that accepts every EVENT and immediately returns EOSE for every REQ. It is useful for connection testing and as a starting point for custom handlers.

func NewRouterHandler

func NewRouterHandler(router *Router) Handler

NewRouterHandler returns a Handler backed by router that broadcasts events between clients. The connection is registered with router on start and unregistered on return, so subscription lifecycle is managed automatically.

func NewSimpleHandler

func NewSimpleHandler(base SimpleHandlerBase) Handler

NewSimpleHandler wraps a SimpleHandlerBase as a Handler. See SimpleHandlerBase for the lifecycle contract.

func NewStorageHandler

func NewStorageHandler(storage Storage, opts *StorageHandlerOptions) Handler

NewStorageHandler returns a Handler that serves EVENT and REQ messages against storage. Pass nil for opts to use defaults.

Behavior:

  • EVENT: Store the event, return OK
  • REQ: Query the storage, return EVENT messages + EOSE
  • CLOSE: No-op — the handler does not manage subscriptions; pair with NewRouterHandler via NewMergeHandler to deliver real-time events
  • COUNT: Query the storage, return COUNT with the count

Once EOSE is sent for a REQ, the handler's job is done for that subscription.

type HandlerFunc

type HandlerFunc func(ctx context.Context, send chan<- *ServerMsg, recv <-chan *ClientMsg) error

HandlerFunc is an adapter to allow the use of ordinary functions as Handler.

func (HandlerFunc) ServeNostr

func (f HandlerFunc) ServeNostr(ctx context.Context, send chan<- *ServerMsg, recv <-chan *ClientMsg) error

ServeNostr calls f(ctx, send, recv).

type InMemoryStorage

type InMemoryStorage struct {
	// contains filtered or unexported fields
}

InMemoryStorage is a simple in-memory implementation of Storage. This is a "brute force" implementation: O(n) for most operations. Suitable for testing and small datasets.

func NewInMemoryStorage

func NewInMemoryStorage() *InMemoryStorage

NewInMemoryStorage creates a new in-memory storage.

func (*InMemoryStorage) Query

func (s *InMemoryStorage) Query(ctx context.Context, filters []*ReqFilter) (iter.Seq[*Event], func() error, func() error)

Query implements Storage.Query.

func (*InMemoryStorage) Store

func (s *InMemoryStorage) Store(ctx context.Context, event *Event) (bool, error)

Store implements Storage.Store.

type MergeHandlerOptions

type MergeHandlerOptions struct {
	// BroadcastTimeout caps how long MergeHandler waits when forwarding a
	// control message (REQ / COUNT / CLOSE) to a single child handler whose
	// recv channel is full. EVENT messages are always best-effort and are
	// not affected by this timeout (a dropped EVENT is signalled to the
	// client via OK accepted=false, prompting a retry).
	//
	// When the timeout fires, the slow child is treated as dead: any pending
	// OK / EOSE / COUNT it owed is advanced as if the handler had exited.
	// The remaining healthy handlers continue to serve the client.
	//
	// A zero value selects [DefaultMergeHandlerBroadcastTimeout] (30s).
	BroadcastTimeout time.Duration

	// Registerer is the Prometheus registry that merge-handler saturation
	// and error signals are registered with (mocrelay_merge_lost_children_total,
	// mocrelay_merge_broadcast_timeouts_total,
	// mocrelay_merge_event_drops_total). If nil, no metrics are collected.
	Registerer prometheus.Registerer
}

MergeHandlerOptions configures a merge handler returned by NewMergeHandler.

All fields are optional. A nil *MergeHandlerOptions is equivalent to a zero-valued MergeHandlerOptions and means "use defaults for everything".

type MetricsStorage

type MetricsStorage struct {
	// contains filtered or unexported fields
}

MetricsStorage wraps a Storage and collects Prometheus metrics for each Store / Query call (mocrelay_events_stored_total, mocrelay_store_duration_seconds, mocrelay_query_duration_seconds, mocrelay_store_errors_total, mocrelay_query_errors_total).

Because mocrelay's Storage interface has multiple implementations (InMemory / Pebble / Composite), MetricsStorage is a decorator rather than an Options-style constructor: wrap whatever Storage you want to measure.

func NewMetricsStorage

func NewMetricsStorage(storage Storage, reg prometheus.Registerer) *MetricsStorage

NewMetricsStorage creates a MetricsStorage wrapping storage. Metrics are registered against reg; if reg is nil, all metric calls no-op and the decorator is effectively a pass-through (no registration happens and no Prometheus collectors are created).

func (*MetricsStorage) Query

func (s *MetricsStorage) Query(ctx context.Context, filters []*ReqFilter) (iter.Seq[*Event], func() error, func() error)

Query implements Storage.Query with metrics collection.

func (*MetricsStorage) Store

func (s *MetricsStorage) Store(ctx context.Context, event *Event) (bool, error)

Store implements Storage.Store with metrics collection.

type Middleware

type Middleware func(Handler) Handler

Middleware wraps a Handler to add functionality.

func NewSimpleMiddleware

func NewSimpleMiddleware(bases ...SimpleMiddlewareBase) Middleware

NewSimpleMiddleware creates a Middleware from one or more SimpleMiddlewareBase.

When multiple bases are provided, they are composed into a single pipeline that uses only one goroutine, regardless of the number of bases. Bases are listed in outermost-first order (like alice.New):

NewSimpleMiddleware(logging, auth)(handler)
// message flow: client → logging → auth → handler

Internally uses a unified select loop with nil channel pattern to handle both recv and send directions in a single goroutine, avoiding deadlocks without requiring buffered channels.

type PebbleStorage

type PebbleStorage struct {
	// contains filtered or unexported fields
}

PebbleStorage implements Storage using Pebble (LSM-tree based KV store).

The *pebble.DB is owned by the caller: PebbleStorage does not open or close the database. The caller is responsible for opening the DB with whatever pebble.Options are appropriate (cache size, bloom filters, event listeners, etc.) and for closing it when done. This lets the caller expose pebble.DB.Metrics directly and pin their own Pebble version independent of mocrelay's go.mod.

func NewPebbleStorage

func NewPebbleStorage(db *pebble.DB, opts *PebbleStorageOptions) *PebbleStorage

NewPebbleStorage wraps an already-open *pebble.DB as a Storage.

The caller retains ownership of db: they are responsible for opening it (with their own pebble.Options) and for calling db.Close() when done. PebbleStorage itself has no Close method.

opts can be nil; there are currently no configurable options.

func (*PebbleStorage) Query

func (s *PebbleStorage) Query(ctx context.Context, filters []*ReqFilter) (iter.Seq[*Event], func() error, func() error)

Query implements Storage.Query.

func (*PebbleStorage) Store

func (s *PebbleStorage) Store(ctx context.Context, event *Event) (bool, error)

Store implements Storage.Store.

type PebbleStorageOptions

type PebbleStorageOptions struct{}

PebbleStorageOptions is reserved for future PebbleStorage configuration. It is currently empty but exists so that adding options later is not a breaking API change. Pass nil for now.

type Relay

type Relay struct {
	// contains filtered or unexported fields
}

Relay wraps a Handler to serve it over HTTP/WebSocket. It implements http.Handler.

func NewRelay

func NewRelay(handler Handler, opts *RelayOptions) *Relay

NewRelay creates a new Relay with the given handler. opts can be nil for default options.

func (*Relay) ServeHTTP

func (r *Relay) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP implements http.Handler.

func (*Relay) Shutdown

func (r *Relay) Shutdown(ctx context.Context) error

Shutdown gracefully shuts down the relay. It closes all WebSocket connections and waits for them to finish. If ctx is canceled before all connections finish, it returns ctx.Err().

func (*Relay) Wait

func (r *Relay) Wait()

Wait blocks until all connections have finished.

type RelayFee

type RelayFee struct {
	Amount int64  `json:"amount"`
	Unit   string `json:"unit"` // e.g., "msats", "sats"
}

RelayFee represents a basic fee amount.

type RelayFees

type RelayFees struct {
	Admission    []*RelayFee             `json:"admission,omitzero"`
	Subscription []*RelaySubscriptionFee `json:"subscription,omitzero"`
	Publication  []*RelayPublicationFee  `json:"publication,omitzero"`
}

RelayFees represents NIP-11 fee schedules.

type RelayInfo

type RelayInfo struct {
	// Basic information
	Name        string `json:"name,omitzero"`
	Description string `json:"description,omitzero"`
	Banner      string `json:"banner,omitzero"`
	Icon        string `json:"icon,omitzero"`
	Pubkey      string `json:"pubkey,omitzero"` // 32-byte hex, admin contact
	Self        string `json:"self,omitzero"`   // 32-byte hex, relay's own pubkey
	Contact     string `json:"contact,omitzero"`

	SupportedNIPs []int  `json:"supported_nips,omitzero"`
	Software      string `json:"software,omitzero"`
	Version       string `json:"version,omitzero"`

	PrivacyPolicy  string `json:"privacy_policy,omitzero"`
	TermsOfService string `json:"terms_of_service,omitzero"`

	// Server limitations
	Limitation *RelayLimitation `json:"limitation,omitzero"`

	// Event retention policies
	// TODO: kinds field has union type: int | [int, int]
	// For now, use a simplified representation
	Retention []*RelayRetention `json:"retention,omitzero"`

	// Content limitations
	RelayCountries []string `json:"relay_countries,omitzero"` // ISO 3166-1 alpha-2

	// Community preferences
	LanguageTags  []string `json:"language_tags,omitzero"` // IETF language tags
	Tags          []string `json:"tags,omitzero"`          // e.g., "sfw-only", "bitcoin-only"
	PostingPolicy string   `json:"posting_policy,omitzero"`

	// Pay-to-Relay
	PaymentsURL string     `json:"payments_url,omitzero"`
	Fees        *RelayFees `json:"fees,omitzero"`
}

RelayInfo represents NIP-11 Relay Information Document. All fields are optional and omitted from JSON when zero-valued.

type RelayLimitation

type RelayLimitation struct {
	MaxMessageLength int64 `json:"max_message_length,omitzero"`
	MaxSubscriptions int   `json:"max_subscriptions,omitzero"`
	MaxLimit         int   `json:"max_limit,omitzero"`
	MaxSubidLength   int   `json:"max_subid_length,omitzero"`
	MaxEventTags     int   `json:"max_event_tags,omitzero"`
	MaxContentLength int   `json:"max_content_length,omitzero"`

	MinPowDifficulty int  `json:"min_pow_difficulty,omitzero"`
	AuthRequired     bool `json:"auth_required,omitzero"`
	PaymentRequired  bool `json:"payment_required,omitzero"`
	RestrictedWrites bool `json:"restricted_writes,omitzero"`

	// created_at limits in seconds
	// Lower: events older than (now - lower_limit) are rejected
	// Upper: events newer than (now + upper_limit) are rejected
	CreatedAtLowerLimit int64 `json:"created_at_lower_limit,omitzero"`
	CreatedAtUpperLimit int64 `json:"created_at_upper_limit,omitzero"`

	DefaultLimit int `json:"default_limit,omitzero"`
}

RelayLimitation represents NIP-11 limitation object.

type RelayOptions

type RelayOptions struct {
	// Logger is the structured logger for connection and error events.
	// Default: slog.Default()
	Logger *slog.Logger

	// MaxMessageLength is the maximum size of a WebSocket message in bytes.
	// Set to a negative value to disable the limit.
	// Default: 100_000 (100 KB)
	MaxMessageLength int64

	// PingInterval is the interval between WebSocket pings.
	// Set to a negative value to disable pings.
	// Default: 30 seconds
	PingInterval time.Duration

	// PingTimeout is the timeout for WebSocket ping responses and write operations.
	// If a pong or write does not complete within this duration, the connection is closed.
	// Default: 10 seconds
	PingTimeout time.Duration

	// ReadRate is the maximum messages-per-second the readLoop will pull
	// off the WebSocket on a single connection. When the per-connection
	// budget is exhausted the readLoop STALLS -- conn.Read is simply not
	// invoked until a token refills -- so back-pressure propagates to the
	// client through the TCP receive window. The relay never spends parse
	// or signature-verification CPU on throttled traffic.
	//
	// This is the readLoop-level total cap and is independent of the
	// per-message-type rate limit middlewares (Event/Req/Count/Auth),
	// which run after parse and let operators tune category-specific
	// budgets. Both layers compose: readLoop acts first as a coarse
	// floodgate, middlewares then refine.
	//
	// Zero or negative disables the limit (default).
	ReadRate float64

	// ReadBurst is the bucket capacity for the readLoop rate limit. Must
	// be >= 1 if ReadRate > 0; the constructor panics on misconfiguration
	// rather than silently degrading. The bucket starts full at OnStart
	// so a freshly-connected client can burst up to ReadBurst messages
	// immediately, then is throttled to ReadRate long-term.
	//
	// Ignored when ReadRate <= 0.
	ReadBurst int

	// Info is the NIP-11 Relay Information Document.
	// If set, the relay will respond to HTTP requests with
	// Accept: application/nostr+json header.
	Info *RelayInfo

	// Registerer is the Prometheus registry that mocrelay's internal
	// metrics are registered with. Setting this single field turns on every
	// metric mocrelay exports from the Relay's own scope:
	//
	//   - Connection / message / event counters on the Relay itself
	//     (mocrelay_connections_*, mocrelay_messages_*, mocrelay_events_received_*,
	//     mocrelay_ws_parse_errors_*, mocrelay_ws_write_errors_*,
	//     mocrelay_ws_write_duration_seconds).
	//   - The unified rejection counter
	//     (mocrelay_rejections_total{middleware, reason}), automatically
	//     installed into the request context so that every middleware's
	//     logRejection call increments it.
	//   - Auth middleware counters (mocrelay_auth_*,
	//     mocrelay_auth_authenticated_connections_current), also installed
	//     into the request context and read by the auth middleware.
	//
	// Metrics owned by types that compose *into* Relay (Router, Storage,
	// CompositeStorage, MergeHandler, MetricsStorage) are registered from
	// their own constructors; pass the same Registerer to each of those
	// Options structs (typically prometheus.DefaultRegisterer) to collect
	// everything under one registry.
	//
	// If nil, no metrics are registered from this Relay and all internal
	// instrumentation no-ops.
	Registerer prometheus.Registerer

	// ConnIDFunc generates a unique connection ID string.
	// If nil, a default monotonic counter ("1", "2", ...) is used.
	ConnIDFunc func() string
}

RelayOptions configures Relay behavior. All fields are optional; the zero value gives sensible defaults.

type RelayPublicationFee

type RelayPublicationFee struct {
	Kinds  []int64 `json:"kinds,omitzero"`
	Amount int64   `json:"amount"`
	Unit   string  `json:"unit"`
}

RelayPublicationFee represents a publication fee for specific kinds.

type RelayRetention

type RelayRetention struct {
	// Simplified: list of single kinds (not ranges)
	// TODO: Support kind ranges like [30000, 39999]
	Kinds []int64 `json:"kinds,omitzero"`

	// Time in seconds. nil means infinity.
	// 0 means the event will not be stored at all.
	Time *int64 `json:"time,omitzero"`

	// Maximum number of events to keep
	Count *int64 `json:"count,omitzero"`
}

RelayRetention represents NIP-11 retention policy. TODO: The "kinds" field in NIP-11 can contain:

  • Single integers: 0, 1, 4
  • Ranges as tuples: [5, 7], [40, 49], [40000, 49999]

Example from NIP-11:

{"kinds": [0, 1, [5, 7], [40, 49]], "time": 3600}

For now, we use a simplified representation with separate fields. A full implementation would need custom JSON marshaling.

type RelaySubscriptionFee

type RelaySubscriptionFee struct {
	Amount int64  `json:"amount"`
	Unit   string `json:"unit"`
	Period int64  `json:"period"` // in seconds
}

RelaySubscriptionFee represents a subscription fee with period.

type ReqFilter

type ReqFilter struct {
	IDs     []string            `json:"ids,omitempty"`
	Authors []string            `json:"authors,omitempty"`
	Kinds   []int64             `json:"kinds,omitempty"`
	Tags    map[string][]string `json:"-"` // handled manually: #e, #p, etc.
	Since   *int64              `json:"since,omitempty"`
	Until   *int64              `json:"until,omitempty"`
	Limit   *int64              `json:"limit,omitempty"`
	Search  *string             `json:"search,omitempty"` // NIP-50
}

ReqFilter represents a filter in REQ/COUNT messages.

func (*ReqFilter) MarshalJSON

func (f *ReqFilter) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for ReqFilter.

func (*ReqFilter) Match

func (f *ReqFilter) Match(ev *Event) bool

Match checks if an event matches this filter.

func (*ReqFilter) UnmarshalJSONFrom

func (f *ReqFilter) UnmarshalJSONFrom(dec *jsontext.Decoder) error

UnmarshalJSONFrom implements json.UnmarshalerFrom for ReqFilter. This handles the dynamic tag fields (#e, #p, etc.) and rejects unknown fields. All field values must be non-null (NIP-01).

func (*ReqFilter) Valid

func (f *ReqFilter) Valid() bool

Valid checks if the filter has valid format. Per NIP-01: list fields must have "one or more values" - empty arrays are invalid.

type RestrictedWritesMode

type RestrictedWritesMode int

RestrictedWritesMode determines how the pubkey list is interpreted.

const (
	// RestrictedWritesModeAllowlist only allows pubkeys in the list.
	RestrictedWritesModeAllowlist RestrictedWritesMode = iota
	// RestrictedWritesModeBlocklist blocks pubkeys in the list.
	RestrictedWritesModeBlocklist
)

type Router

type Router struct {
	// contains filtered or unexported fields
}

Router manages client connections and subscriptions. It routes events to clients based on their subscription filters.

func NewRouter

func NewRouter(opts *RouterOptions) *Router

NewRouter creates a new Router. opts can be nil for default options.

func (*Router) Broadcast

func (r *Router) Broadcast(event *Event)

Broadcast sends an event to all matching subscriptions. This is best-effort: if a connection's channel is full, the message is dropped.

func (*Router) Register

func (r *Router) Register(sendCh chan<- *ServerMsg) string

Register registers a new connection and returns the connection ID. The sendCh is used to send messages to this connection.

func (*Router) Subscribe

func (r *Router) Subscribe(connID, subID string, filters []*ReqFilter)

Subscribe registers a subscription for a connection. If the subscription ID already exists, it will be replaced.

func (*Router) Unregister

func (r *Router) Unregister(connID string)

Unregister removes a connection and all its subscriptions.

func (*Router) Unsubscribe

func (r *Router) Unsubscribe(connID, subID string)

Unsubscribe removes a subscription from a connection.

type RouterOptions

type RouterOptions struct {
	// Registerer is the Prometheus registry that Router's internal metrics
	// are registered with (mocrelay_router_messages_dropped_total and
	// mocrelay_router_subscriptions_current). If nil, no metrics are
	// collected.
	Registerer prometheus.Registerer
}

RouterOptions configures Router behavior. All fields are optional; the zero value gives sensible defaults.

type SearchIndex

type SearchIndex interface {
	// Index adds or updates an event in the search index.
	Index(ctx context.Context, event *Event) error

	// Search returns event IDs matching the query, ordered by relevance.
	// Results are limited to the specified count.
	Search(ctx context.Context, query string, limit int64) (eventIDs []string, err error)

	// Delete removes an event from the search index.
	Delete(ctx context.Context, eventID string) error
}

SearchIndex defines the interface for full-text search indexing.

type ServerMsg

type ServerMsg struct {
	Type string

	// EVENT: subscription ID and event
	SubscriptionID string
	Event          *Event

	// OK: event ID, success flag, and message
	EventID  string
	Accepted bool
	Message  string

	// COUNT: count result
	Count       uint64
	Approximate *bool
}

ServerMsg represents a message from relay to client. This is a union type - check Type field to determine which fields are valid.

func NewServerAuthMsg

func NewServerAuthMsg(challenge string) *ServerMsg

NewServerAuthMsg creates an AUTH message (challenge).

func NewServerClosedMsg

func NewServerClosedMsg(subID string, message string) *ServerMsg

NewServerClosedMsg creates a CLOSED message.

func NewServerCountMsg

func NewServerCountMsg(subID string, count uint64, approximate *bool) *ServerMsg

NewServerCountMsg creates a COUNT message.

func NewServerEOSEMsg

func NewServerEOSEMsg(subID string) *ServerMsg

NewServerEOSEMsg creates an EOSE message.

func NewServerEventMsg

func NewServerEventMsg(subID string, event *Event) *ServerMsg

NewServerEventMsg creates an EVENT message.

func NewServerNoticeMsg

func NewServerNoticeMsg(message string) *ServerMsg

NewServerNoticeMsg creates a NOTICE message.

func NewServerOKMsg

func NewServerOKMsg(eventID string, accepted bool, message string) *ServerMsg

NewServerOKMsg creates an OK message.

func (*ServerMsg) MarshalJSON

func (m *ServerMsg) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for ServerMsg.

type SimpleHandlerBase

type SimpleHandlerBase interface {
	// OnStart is called when a new connection is established.
	// The returned context is passed to subsequent calls.
	// The returned ServerMsg (if non-nil) is sent to the client (e.g., AUTH challenge).
	// Return an error to reject the connection.
	OnStart(ctx context.Context) (context.Context, *ServerMsg, error)

	// OnEnd is called when the connection is closing.
	// This is always called, even if HandleMsg returned an error.
	// The returned ServerMsg (if non-nil) is sent to the client.
	// Use this for cleanup (e.g., removing subscriptions).
	OnEnd(ctx context.Context) (*ServerMsg, error)

	// HandleMsg is called for each client message.
	// Return an iterator of server messages to send back.
	// Return an error to terminate the connection.
	HandleMsg(ctx context.Context, msg *ClientMsg) (iter.Seq[*ServerMsg], error)
}

SimpleHandlerBase is the interface for handlers that process messages one at a time. This is easier to implement than Handler for most use cases.

The lifecycle is:

  1. OnStart is called when the connection is established
  2. HandleMsg is called for each client message
  3. OnEnd is called when the connection is closing (always called, even on error)

type SimpleMiddlewareBase

type SimpleMiddlewareBase interface {
	// OnStart is called when a new connection is established.
	// The returned context is passed to subsequent calls.
	// The returned ServerMsg (if non-nil) is sent to the client (e.g., AUTH challenge).
	// Return an error to reject the connection.
	OnStart(ctx context.Context) (context.Context, *ServerMsg, error)

	// OnEnd is called when the connection is closing.
	// This is always called, even if Handle* returned an error.
	// The returned ServerMsg (if non-nil) is sent to the client.
	// Use this for cleanup.
	OnEnd(ctx context.Context) (*ServerMsg, error)

	// HandleClientMsg processes a client message.
	// Returns:
	//   - (out, nil, nil): pass out to downstream handler
	//   - (nil, resp, nil): send resp to client, don't pass downstream
	//   - (nil, nil, nil): drop the message
	//   - (_, _, err): error, connection will be terminated
	HandleClientMsg(ctx context.Context, msg *ClientMsg) (out *ClientMsg, resp *ServerMsg, err error)

	// HandleServerMsg processes a server message from downstream.
	// Returns:
	//   - (out, nil): send out to client
	//   - (nil, nil): drop the message
	//   - (_, err): error, connection will be terminated
	HandleServerMsg(ctx context.Context, msg *ServerMsg) (out *ServerMsg, err error)
}

SimpleMiddlewareBase is the interface for middlewares that process messages one at a time. This is easier to implement than writing a full Middleware for most use cases.

The lifecycle is:

  1. OnStart is called when the connection is established
  2. HandleClientMsg is called for each client message (recv direction)
  3. HandleServerMsg is called for each server message (send direction)
  4. OnEnd is called when the connection is closing (always called, even on error)

Message handling rules:

  • HandleClientMsg returns (out, resp, err):
  • out != nil: pass out to downstream handler
  • resp != nil: send resp to client (without passing to downstream)
  • out == nil && resp == nil: drop the message
  • HandleServerMsg returns (out, err):
  • out != nil: send out to client
  • out == nil: drop the message

func NewAuthMiddlewareBase

func NewAuthMiddlewareBase(relayURL string, opts *AuthMiddlewareOptions) SimpleMiddlewareBase

NewAuthMiddlewareBase creates a middleware base that requires NIP-42 authentication.

relayURL is the URL of this relay (used for relay tag validation). opts can be nil for default options.

func NewAuthRateLimitMiddlewareBase added in v0.16.0

func NewAuthRateLimitMiddlewareBase(rate float64, burst int) SimpleMiddlewareBase

NewAuthRateLimitMiddlewareBase returns a middleware base that rate-limits incoming AUTH (NIP-42) messages on a per-connection basis using a token bucket.

rate is the long-term sustained rate of AUTH messages allowed per connection, in AUTHs per second; must be positive. burst is the maximum number of AUTH messages a connection may submit in a tight window before rate-limiting kicks in. The bucket starts full at connection start; must be positive.

When the bucket is empty, the offending AUTH is dropped and a `["OK", <event_id>, false, "rate-limited: too many auth attempts"]` is sent to the client. NIP-42 specifies AUTH responses are OK messages, so the response shape matches a successful AUTH except for accepted=false.

EVENT / REQ / CLOSE / COUNT messages pass through untouched -- compose with the matching per-type middleware (Event/Req/Count) if you need to limit those independently.

Use this middleware to defend against AUTH challenge-response brute force; a typical setting is a low rate (e.g. 0.1 = one attempt every 10 seconds long-term) with a small burst (e.g. 3) so legitimate reconnects still work but credential stuffing is throttled.

Each connection gets its own token bucket via OnStart + context.

Panics if rate <= 0 or burst < 1.

func NewCountRateLimitMiddlewareBase added in v0.16.0

func NewCountRateLimitMiddlewareBase(rate float64, burst int) SimpleMiddlewareBase

NewCountRateLimitMiddlewareBase returns a middleware base that rate-limits incoming COUNT (NIP-45) messages on a per-connection basis using a token bucket.

rate is the long-term sustained rate of COUNT messages allowed per connection, in COUNTs per second; must be positive. burst is the maximum number of COUNT messages a connection may submit in a tight window before rate-limiting kicks in. The bucket starts full at connection start; must be positive.

When the bucket is empty, the offending COUNT is dropped and a `["CLOSED", <sub_id>, "rate-limited: too many counts"]` is sent to the client. EVENT / REQ / CLOSE / AUTH messages pass through untouched -- compose with the matching per-type middleware (Event/Req/Auth) if you need to limit those independently.

COUNT can be expensive to compute (it scans the same indexes a REQ would but cannot stream early), so a tighter rate than REQ is often appropriate. Each connection gets its own token bucket via OnStart + context.

Panics if rate <= 0 or burst < 1.

func NewCreatedAtLimitsMiddlewareBase

func NewCreatedAtLimitsMiddlewareBase(lowerLimit, upperLimit int64) SimpleMiddlewareBase

NewCreatedAtLimitsMiddlewareBase returns a middleware base that limits the created_at timestamp of events.

lowerLimit is the maximum age in seconds (how far into the past is allowed). upperLimit is the maximum seconds into the future allowed. Both must be non-negative.

func NewEventRateLimitMiddlewareBase added in v0.16.0

func NewEventRateLimitMiddlewareBase(rate float64, burst int) SimpleMiddlewareBase

NewEventRateLimitMiddlewareBase returns a middleware base that rate-limits incoming EVENT messages on a per-connection basis using a token bucket.

rate is the long-term sustained rate of EVENT messages allowed per connection, in events per second; must be positive. burst is the maximum number of EVENT messages a connection may submit in a tight window before rate-limiting kicks in. The bucket starts full at connection start; must be positive.

When the bucket is empty, the offending EVENT is dropped and a `["OK", <id>, false, "rate-limited: too many events"]` is sent to the client (NIP-01 standard prefix). REQ / CLOSE / AUTH / COUNT messages pass through untouched -- compose with the matching per-type middleware (Req/Count/Auth) if you need to limit those independently.

The token bucket is created in OnStart and stored in the request context, so each connection has its own independent bucket. Counters are not shared across connections.

Panics if rate <= 0 or burst < 1.

func NewExpirationMiddlewareBase

func NewExpirationMiddlewareBase() SimpleMiddlewareBase

NewExpirationMiddlewareBase returns a middleware base implementing NIP-40 (Expiration Timestamp). Events with an "expiration" tag are rejected if expired on receipt, and dropped (not delivered) if expired on send.

func NewKindAllowlistMiddlewareBase added in v0.15.0

func NewKindAllowlistMiddlewareBase(kinds []int64) SimpleMiddlewareBase

NewKindAllowlistMiddlewareBase returns a middleware base that rejects events whose kind is NOT in kinds. This is useful for restricting a relay to a well-known set of kinds (e.g., the kinds documented in the NIPs README).

An empty allowlist rejects all events. Range-style kinds (e.g., NIP-90 Job Request 5000-5999) are expanded into individual entries by the caller:

kinds := []int64{0, 1, 3}
for k := int64(5000); k <= 5999; k++ {
	kinds = append(kinds, k)
}
mw := NewKindAllowlistMiddlewareBase(kinds)

func NewKindDenylistMiddlewareBase

func NewKindDenylistMiddlewareBase(kinds []int64) SimpleMiddlewareBase

NewKindDenylistMiddlewareBase returns a middleware base that rejects events whose kind is in kinds. This is useful for legal compliance (e.g., rejecting DM-related kinds for Japanese telecommunications law compliance).

func NewMaxContentLengthMiddlewareBase

func NewMaxContentLengthMiddlewareBase(maxLen int) SimpleMiddlewareBase

NewMaxContentLengthMiddlewareBase returns a middleware base that limits the number of Unicode characters (not bytes) in event content.

maxLen must be a positive integer.

func NewMaxEventTagsMiddlewareBase

func NewMaxEventTagsMiddlewareBase(maxTags int) SimpleMiddlewareBase

NewMaxEventTagsMiddlewareBase returns a middleware base that limits the number of tags in an event. maxTags must be a positive integer.

func NewMaxLimitMiddlewareBase

func NewMaxLimitMiddlewareBase(maxLimit, defaultLimit int64) SimpleMiddlewareBase

NewMaxLimitMiddlewareBase returns a middleware base that clamps the limit value in REQ filters and applies a default when unset.

maxLimit is the maximum allowed limit value (must be positive). defaultLimit is applied when no limit is specified (must be positive and <= maxLimit).

func NewMaxSubidLengthMiddlewareBase

func NewMaxSubidLengthMiddlewareBase(maxLen int) SimpleMiddlewareBase

NewMaxSubidLengthMiddlewareBase returns a middleware base that limits the length of subscription IDs. maxLen must be a positive integer.

func NewMaxSubscriptionsMiddlewareBase

func NewMaxSubscriptionsMiddlewareBase(maxSubs int) SimpleMiddlewareBase

NewMaxSubscriptionsMiddlewareBase returns a middleware base that limits the number of concurrent subscriptions per connection. maxSubs must be a positive integer.

func NewMinPowDifficultyMiddlewareBase

func NewMinPowDifficultyMiddlewareBase(minDifficulty int, checkCommitment bool) SimpleMiddlewareBase

NewMinPowDifficultyMiddlewareBase creates a middleware base that validates Proof of Work (NIP-13).

Parameters:

  • minDifficulty: minimum number of leading zero bits required in event ID
  • checkCommitment: if true, also validates the nonce tag's target difficulty. This rejects events whose committed target is below minDifficulty even if their actual difficulty is sufficient, preventing spammers from getting lucky with low-target mining.

func NewProtectedEventsMiddlewareBase

func NewProtectedEventsMiddlewareBase() SimpleMiddlewareBase

NewProtectedEventsMiddlewareBase returns a middleware base implementing NIP-70 (Protected Events): events with a ["-"] tag can only be published by their authenticated author.

This middleware requires NIP-42 authentication to be enabled upstream. If no authentication state is found in the context, protected events are always rejected.

func NewReqRateLimitMiddlewareBase added in v0.16.0

func NewReqRateLimitMiddlewareBase(rate float64, burst int) SimpleMiddlewareBase

NewReqRateLimitMiddlewareBase returns a middleware base that rate-limits incoming REQ messages on a per-connection basis using a token bucket.

rate is the long-term sustained rate of REQ messages allowed per connection, in REQs per second; must be positive. burst is the maximum number of REQ messages a connection may submit in a tight window before rate-limiting kicks in. The bucket starts full at connection start; must be positive.

When the bucket is empty, the offending REQ is dropped and a `["CLOSED", <sub_id>, "rate-limited: too many subscriptions"]` is sent to the client. EVENT / CLOSE / AUTH / COUNT messages pass through untouched -- compose with the matching per-type middleware (Event/Count/Auth) if you need to limit those independently.

Each connection gets its own token bucket via OnStart + context.

Panics if rate <= 0 or burst < 1.

func NewRestrictedWritesMiddlewareBase

func NewRestrictedWritesMiddlewareBase(mode RestrictedWritesMode, pubkeys []string) SimpleMiddlewareBase

NewRestrictedWritesMiddlewareBase returns a middleware base that restricts which pubkeys can write events.

mode determines whether the list is an allowlist or blocklist. pubkeys is the list of pubkeys to allow or block.

type Storage

type Storage interface {
	// Store saves an event and returns whether it was actually stored.
	// Returns false for duplicates, older replaceable events, or deleted events.
	Store(ctx context.Context, event *Event) (stored bool, err error)

	// Query returns an iterator over events matching any of the filters.
	// Results are sorted by created_at DESC, then id ASC (lexical).
	// The first filter's limit is applied globally (NIP-01).
	//
	// Returns:
	//   - events: iterator over matching events (use with for-range)
	//   - err: call after iteration to check for errors
	//   - close: call to release resources (use with defer)
	//
	// Example:
	//   events, errFn, closeFn := storage.Query(ctx, filters)
	//   defer closeFn()
	//   for event := range events {
	//       // process event
	//   }
	//   if err := errFn(); err != nil {
	//       return err
	//   }
	Query(ctx context.Context, filters []*ReqFilter) (events iter.Seq[*Event], err func() error, close func() error)
}

Storage defines the interface for event storage.

type StorageHandlerOptions added in v0.9.0

type StorageHandlerOptions struct {
	// SlowQueryThreshold sets the total-duration threshold above which a REQ
	// or COUNT subscription is logged at Warn via [LoggerFromContext]. The
	// emitted log carries a breakdown (scan vs send-wait time for REQ) so
	// operators can distinguish a genuinely heavy query from a client that
	// is failing to drain its send buffer — the two are entangled because
	// iter.Seq is pull-driven and pauses the storage iterator while yield
	// is blocked, so having both numbers side-by-side is the most reliable
	// signal of which side owns the stall.
	//
	// A zero value selects [DefaultStorageHandlerSlowQueryThreshold] (1s).
	// A negative value disables slow-query logging entirely.
	SlowQueryThreshold time.Duration

	// QueryTimeout aborts a REQ or COUNT whose total duration exceeds it.
	// When the timeout fires, the storage handler stops iterating, sends a
	// CLOSED message with reason "error: query timeout", and returns —
	// neither the trailing EOSE (for REQ) nor the COUNT reply is emitted.
	// The slow-query log (if enabled) still fires with completed=false, so
	// the abort is recorded alongside other slow subscriptions.
	//
	// The context passed to Storage.Query is wrapped with
	// [context.WithTimeout], so any Storage implementation that respects
	// ctx can short-circuit its own scan; implementations that don't (e.g.
	// Pebble) are stopped at the next event boundary where the iteration
	// loop polls ctx.Err.
	//
	// The primary use case is a live but slow-consuming client — one that
	// keeps the WebSocket alive (so ping_timeout never fires) yet drains
	// EVENTs so slowly that a broad REQ can stall the handler's iterator
	// for hours. Setting QueryTimeout gives the relay a hard ceiling.
	//
	// A zero value disables the timeout (default, preserves prior
	// behavior). A negative value also disables it. When non-zero, it
	// should typically be set well above SlowQueryThreshold so the
	// threshold logs fire first and give operators a chance to observe
	// slow REQs before they are aborted.
	QueryTimeout time.Duration
}

StorageHandlerOptions configures the storage handler returned by NewStorageHandler.

All fields are optional. A nil *StorageHandlerOptions is equivalent to a zero-valued StorageHandlerOptions and means "use defaults for everything".

type Tag

type Tag []string

Tag represents a tag in a Nostr event. The first element is the tag name, followed by optional values.

func (Tag) Key

func (t Tag) Key() string

Key returns the tag name (first element).

func (*Tag) UnmarshalJSONFrom

func (t *Tag) UnmarshalJSONFrom(dec *jsontext.Decoder) error

UnmarshalJSONFrom implements json.UnmarshalerFrom to reject null elements. NIP-01 requires tags to be "arrays of non-null strings".

func (Tag) Value

func (t Tag) Value() string

Value returns the first value of the tag (second element).

Directories

Path Synopsis
cmd
examples/kitchen-sink command
Kitchen-sink relay example.
Kitchen-sink relay example.
examples/minimal command
Minimal relay example.
Minimal relay example.
examples/nop command
Nop relay example.
Nop relay example.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL