Download the PHP package innis/nostr-relay-selection without Composer

On this page you can find all versions of the php package innis/nostr-relay-selection. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package nostr-relay-selection

Nostr Relay Selection

CI

A PHP library for routing Nostr events to and from relays. Implements outbox-model publish routing, read routing, author-set-cover, relay hint selection, NIP-65 inbox/outbox/DM list parsing, NIP-17 DM-inbox handling, and URL classification — as pure functions, with zero runtime dependencies.

Why this library?

When a client publishes a kind 1 reply, which relays should it actually send to? When it queries an author's notes, which relays will return them? When it picks a relay hint for an e tag, which one will work for the recipient? These are not trivial questions in the outbox model.

innis/nostr-relay-selection answers them as pure functions over the user's relay-list events. It does not open WebSocket connections, does not depend on a relay pool, does not depend on innis/nostr-core, and does not depend on any other Nostr library. Feed it events and a context, get back a deterministic list of relays.

Design reasoning: a spec, not an engine

The Nostr ecosystem already has several relay-selection implementations — NDK's OutboxTracker, rust-nostr's gossip crate, go-nostr's sdk hints DB, Coracle's welshman/router. They are all engines: stateful, heuristic, async, coupled to a pool and to learned data. They make pragmatic, useful tradeoffs, and none of them are deterministic.

This library makes the opposite tradeoff. It is a policy specification:

Engines and specs compose. This library is the policy; an engine wraps it with caching, pool state, fallbacks, scoring, or whatever else a runtime needs. The two layers stay separate so the policy stays portable and auditable.

Requirements

No PHP extensions are required. No system libraries. No Composer dependencies.

Installation

Quick Start

All services are pure static methods. Inputs are typed context objects; outputs are typed route objects (with a branch enum + a list of RelayUrl) or lists of RelayUrl directly.

For a runnable end-to-end demonstration against live relay-list events from four real Nostr identities (loaded from tests/corpus/real-world/), see example.php:

Constructing typed objects from raw JSON

Every type the routing services consume has a static fromRaw(mixed): ?self factory that validates the JSON-shaped input and returns null on malformed data:

Use these at your application's adapter boundary. The lib never returns null from happy-path routing — null from fromRaw/fromHex/fromString always means "the input you gave me was not a valid X."

Route a publish

Given an event and the user's relay-list events (kind 10002 / 10050), decide which relays to publish to. Returns a PublishRoute whose getBranch() reports the policy applied and whose getRelays() lists the targets (which may be empty, or null for the Dm branch — see below).

The lib does not implement NIP-37 itself — kind 10013's relay list is NIP-44-encrypted inside content, and decryption requires a signer + crypto, both out of scope here. Callers that want NIP-37-aware draft routing must fetch and decrypt kind 10013 themselves and pass the resulting URLs into PublishContext::$privateContentRelays.

Route a read

Given a set of filters, decide which relays to subscribe to. Returns a ReadRoute with a ReadBranch enum and a list of relays.

Filter pattern detection

ReadRouter internally calls FilterPatternClassifier::classify($filters) to map a filter set to a ReadBranch (Search, DmInbox, General). The primitive is exposed so callers can inspect or branch on the pattern without invoking the full router.

Author read: greedy set cover

Given a list of author pubkeys, decide which outbox relays cover them. Uses greedy set-cover so two authors who share a relay are queried together; chunks each plan if the author count exceeds maxAuthorsPerFilter; falls back to caller-supplied relays for authors with no NIP-65 list.

Pick a relay hint

For e / p / q tags, pick a single relay URL the recipient is likely to read. Prefers the intersection of the user's outbox with the target's inbox, falls back to either side's first relay, returns null if neither side has a list.

URL classification

Pure predicates on RelayUrl for use in caller-side filtering. The library does not apply these itself — they're exposed so consumers can compose filters without re-implementing host detection.

Caller-owned lists (blocked, search, private content)

Three kinds of user-owned data are passed as pre-extracted URL lists rather than as raw events:

Kind NIP Field on context Notes
10006 NIP-65 blockedRelays (on every context) Subtracted from every route output uniformly.
10007 NIP-50 searchRelays (on ReadContext) Unioned into the Search branch alongside caller-supplied relays.
10013 NIP-37 privateContentRelays (on PublishContext) Encrypted content — caller must decrypt before passing.

For 10006 and 10007 the caller pre-extracts URLs from their cached event using RelayListExtractor::blocked($event->getTags()) or ::search($event->getTags()). For 10013, the caller does NIP-44 decryption themselves and passes the resulting URLs.

This mirrors the existing userRelayUrls pattern on ReadContext: user-owned data is the caller's responsibility to extract; library policy is to apply.

Other operations

Service Purpose
AuthorRelaySelector::inbox Pick inbox relays for one author (kind 10002 read/both markers).
AuthorRelaySelector::outbox Pick outbox relays for one author (kind 10002 write/both markers).
AuthorRelaySelector::dm Pick DM relays for one author (kind 10050 relay tags).
ZapRequestRelaySelector Merge zapper and recipient inbox relays for a zap request.
RelayHintSelector Pick one relay URL hint for an e/p/q tag.
FilterPatternClassifier::classify Classify a filter set as Search, DmInbox, or General.
FilterPatternClassifier::sharedGiftWrapRecipient If every filter is {kinds: [1059], #p: [singleRecipient]} with the same recipient, return that PublicKey; otherwise null. Useful for detecting DM-target reads without invoking the full router.
MissingRelayListFinder For inbox-fanout events, list p-tagged pubkeys whose relay list you do not yet have cached.
EventSelector::newestByPubkeyAndKind Find the newest event for a given (pubkey, kind) tuple in a heterogeneous event array. Used internally by every routing service and exposed for callers building their own cache layers. Returns ?Event.
RelayListExtractor::inbox/outbox/dm Parse r and relay tags from kind 10002 / 10050 events.
RelayListExtractor::blocked/search Parse relay tags from kind 10006 / 10007 events.
RelaySetBuilder::build Merge any number of relay sources into one deduplicated list, preserving first-seen order.
RelaySetBuilder::subtract Remove URLs in a blocklist from a relay set.
RelayUrl::tryFromString Normalise an arbitrary URL string. Lowercases scheme and host, strips default ports and trailing slashes. Rejects non-wss(?), fragments, %20 in paths, malformed hostnames, out-of-range ports, concatenated URLs, and inputs over 200 chars.
RelayUrl::isOnion/isLoopback/isLocalAddr/isInsecure Pure URL classification predicates.
RelayUrl::equals Compare two RelayUrl instances for canonical-string equality.
Event::tryFromRaw / Filter::tryFromRaw / Tag::tryFromRaw Validate and construct typed objects from JSON-shaped arrays. Return null on malformed input.
PublicKey::tryFromHex / PublicKey::toHex / PublicKey::equals Construct from / serialise to / compare hex pubkeys.

Routing rules

The complete policy in one place. Each rule is encoded in the source and locked by a corpus vector.

What goes in EventKind

A kind appears in EventKind if and only if the routing policy distinguishes it from arbitrary unknown kinds. Concretely, a kind belongs in the enum when at least one of these is true:

The rule is drives a branch in PublishRouter::route, not "has any routing rule." Two NIPs define routing rules for kinds that are nonetheless absent from EventKind, and that's deliberate:

Pure vocabulary-only constants — Report (1984), LiveActivity (30311), Job (5000-5999), etc. — never enter the routing spec at all. A future kind registry package (or innis/nostr-core) is the right home for those names. (ProfileMetadata (0) and FollowList (3) earned their place by being indexed kinds; the rule remains "drives a routing decision, or out.")

Publish branches

PublishRouter::route($event, $context) dispatches on event kind into one of three branches. Every output also has the user's blockedRelays subtracted.

Branch Triggering kinds Output relays
Dm 1059 (GiftWrap) Per recipient (p tag), the recipient's newest kind-10050 inbox relays. Empty if the recipient has no kind 10050 (per NIP-17 "shouldn't try").
Draft 30024 (LongformDraft), 30403 (ClassifiedListingDraft), 31234 (DraftEvent) privateContentRelays if non-empty, otherwise user's outbox (from newest kind 10002, write/both markers).
General Everything else User's outbox, plus — for isInboxFanout kinds — recipient inbox fanout (capped per recipient), plus — for isIndexed kinds — indexerRelays.

isInboxFanout includes: 1 (ShortNote), 6 (Repost), 7 (Reaction), 16 (GenericRepost), 24 (PublicMessage), 1111 (Comment), 9802 (Highlight). For these kinds, every p-tagged recipient's newest kind-10002 inbox (read/both markers; unmarked entries count as both) is unioned in. If the recipient has no usable inbox — either because kind 10002 is absent OR because the cached kind 10002 has no read/both entries (write-only) — the lib falls back to the position-[2] relay hint on the p tag, if any. Per-recipient cap defaults to 3 (PublishContext::DEFAULT_PER_RECIPIENT_CAP).

isIndexed includes: 0 (ProfileMetadata), 3 (FollowList), 10002 (RelayList), 10050 (DmRelayList). When publishing one of these, indexerRelays are unioned into the General output so well-known indexers (purplepag.es, user.kindpag.es, relay.nos.social, etc.) see the updated event. The set matches welshman's INDEXED_KINDS.

Read branches

ReadRouter::route($context) dispatches on filter shape. Pattern detection is also exposed as a primitive via FilterPatternClassifier::classify($filters), which returns ReadBranch directly. Every output has blockedRelays subtracted.

Branch Triggering filter shape Output relays
Search Any filter has a non-null search field searchRelays ∪ callerRelays.
DmInbox Every filter is exactly {kinds: [1059], #p: [singleRecipient]} and the recipient is the same across filters Recipient's newest kind-10050 inbox relays, or null if none are available (see below).
General Anything else (including no filters) userRelayUrls ∪ callerRelays.

DM branches return null with no fallback

The DM branches — ReadBranch::DmInbox and PublishBranch::Dm — are the only branches whose relays can be null. Both return null when the recipient has no cached kind-10050 event, when the kind-10050 event extracts to no relays, or when blocking removes every DM relay the recipient declared. There is no fallback to callerRelays, userRelayUrls, or any other source — a gift-wrap publish or subscription cannot quietly redirect to relays the recipient has not authorised without leaking metadata about who the caller is talking to. A null result means "the caller cannot honour this DM; do not act."

The other branches never return null; they may return an empty array if their inputs are all empty (e.g. user has no outbox and the caller supplied no relays), which signals caller misconfiguration rather than a routing refusal:

relays value Meaning
null Branch is Dm / DmInbox and no DM-relay route exists. By design. Do not act.
[] Branch is non-DM and the inputs produced nothing. Caller misconfiguration.
non-empty Use these relays.

Unknown kinds

Any kind not specifically named in the policy routes as PublishBranch::General with no recipient fanout and no indexer relays — i.e. to the user's outbox only. This is deliberate: the spec-derived default for an unrecognised kind is "publish to the author's own outbox, nothing more."

Two paths to change that:

Blocklist application

blockedRelays is subtracted from every routing output uniformly. The list is the caller's responsibility to pre-extract from kind 10006 events (use RelayListExtractor::blocked($event->getTags())). The library never connects to relays, so "blocked" here means "filtered out of routing outputs"; the engine layer above the library should also refuse to open connections to these URLs.

Search relays

searchRelays is the caller's pre-extracted list from their kind 10007 event. It is unioned (alongside callerRelays) into the Search branch only. It is not consulted for General or DmInbox reads.

Architecture

There is no Application layer because no infrastructure ports are needed. There is no Infrastructure layer because there are no adapters. Routing is pure protocol logic.

Relationship to innis/nostr-core

innis/nostr-relay-selection deliberately does not depend on innis/nostr-core. The two libraries are independent and can be used together or separately.

PublicKey, RelayUrl, Event, Filter and Tag are re-declared here in minimal form, because re-declaring five small types is preferable to forcing every consumer of relay selection to adopt nostr-core — and a particular version of it. The reasoning, and what the duplication costs, is recorded in ADR-0002. If you are already using nostr-core, convert at the boundary (PublicKey::tryFromHex($core->toHex())).

What this library does NOT do

Deliberate omissions. These belong in the engine layer wrapping the lib, not in the lib itself.

Testing

The compliance suite loads JSON test vectors under tests/corpus/. The corpus is the spec — any divergence between implementations is a test failure.

The corpus includes signed events imported verbatim from rust-nostr/nostr's gossip test suite under tests/corpus/external-fixtures/rust-nostr/. Each fixture carries a source field naming the upstream test it came from. The imported events are unmodified; our policy outputs are independently derived from the NIPs and may differ from rust-nostr's. Sharing fixture inputs across implementations makes any such divergence inspectable.

Anti-patterns

Architecture decisions

Design rationale — the deliberate choices that read like smells until you know why, including why this library re-declares the protocol types rather than depending on innis/nostr-core — lives in version-controlled records under docs/adr/.

License

MIT License. See LICENSE file for details.


All versions of nostr-relay-selection with dependencies

PHP Build Version
Package Version
Requires php Version ^8.4
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package innis/nostr-relay-selection contains the following files

Loading the files please wait ...