Minor Changes
-
#756
570a7ccThanks @Priyanshubhartistm! - feat: add relay-load-aware adaptive PoW difficulty (NIP-13)Adds
limits.event.powsettings that scale the required proof-of-work difficulty between a
configured floor and ceiling based on the observed event rate, in place of the existing static
eventId.minLeadingZeroBitsvalue (the staticpubkey.minLeadingZeroBitscheck is unaffected and
still enforced independently). The event rate is tracked per worker process with the same EWMA
shape already used by the relay's rate limiter. Disabled by default
(limits.event.pow.enabled: false), so existing static PoW configuration is unaffected unless
explicitly opted in. -
#641
837540bThanks @Ferryx349! - feat: add disabled-by-default admin API with password auth, session, and health endpoints -
#666
44f3bb4Thanks @Ferryx349! - feat: add admin observability dashboard with Grafana embed and provisioned metrics panels -
#653
8ab4825Thanks @Ferryx349! - feat: add OpenTelemetry metrics bootstrap with OTLP export for Prometheus -
#661
237b1a4Thanks @Ferryx349! - feat: instrument event and websocket handlers with OpenTelemetry metrics -
#662
36d95ccThanks @Ferryx349! - feat: add Prometheus-backed admin metrics SSE endpoint -
#753
7c2875eThanks @Ferryx349! - feat(admin): add GET /admin/network-health endpoint -
#764
ff43cd3Thanks @Ferryx349! - feat(admin): add Network Health panel to observability dashboardAdds a dashboard section that renders the latest NIP-66 probe snapshot with per-target DNS, TLS, WebSocket RTT, and NIP-11 status.
-
#784
c6c4c4fThanks @Ferryx349! - feat(admin): add GET/PATCH /admin/notifications API for operator alert configCloses #760.
-
#787
de7c9dfThanks @Ferryx349! - feat(admin): add notifications console page for operator alertsAdds a Notifications view to the admin dashboard for configuring targets and
event toggles, testing delivery, and browsing the delivery log via the admin API.Closes #761
-
#690
f70adf2Thanks @Ferryx349! - feat: add authenticated admin settings API endpoints -
#706
841833fThanks @Ferryx349! - feat: add settings editor tab to admin dashboard UI -
#770
07524ebThanks @Priyanshubhartistm! - feat: add WoT-weighted NIP-56 content reportingAccepts and stores kind-1984 report events, weighting each report by the reporter's WoT distance
fromwot.seedPubkey(full weight for a direct follow, halving each additional hop, zero for a
pubkey outside the trust graph). Reports from anip56.trustedModeratorspubkey always get maximum
weight and are flagged actionable, ready for a future management-API surface to act on; every other
report is stored for manual review only. Disabled by default (nip56.enabled: false). -
#734
fd7f56aThanks @Priyanshubhartistm! - feat(dvm): dispatch pending DVM jobs to worker processes and publish kind 6000-6999 results back -
#729
275c30cThanks @Priyanshubhartistm! - feat(dvm): trap NIP-90 job request events (kind 5000-5999) and record them via the job repository -
#727
d00eb42Thanks @Priyanshubhartistm! - feat(dvm): add job persistence migration and repository for DVM job state -
#737
9f36191Thanks @Priyanshubhartistm! - feat(dvm): add NIP-89 handler recommendation/information kinds and verify passive accept-and-serve -
#721
11ec673Thanks @Priyanshubhartistm! - feat(dvm): add worker registry settings and dvm-orchestrator process topology -
#587
30fa252Thanks @Anshumancanrock! - Add NIP-50 full-text search support with PostgreSQLtsvector/GINindexing.Clients can now include a
searchfield in REQ filter objects to perform full-text
queries against event content. Results are ranked by relevance (ts_rank) instead
of the usualcreated_atordering, per the NIP-50 specification.Features:
- New
searchfilter field accepted in REQ messages - PostgreSQL GIN index on
to_tsvector('simple', event_content)for fast full-text lookups - Configurable text-search language (defaults to
simple, supportsenglish,spanish, etc.) - Configurable max search query length for abuse prevention
- NIP-50 listed in NIP-11 relay information document
- Search can be combined with all existing filter fields (kinds, authors, tags, etc.)
- New
-
#702
e172cceThanks @Anshumancanrock! - feat(nip42): enforce authentication on reads for restricted event kinds (encrypted DMs, gift wraps) across REQ, live broadcasts and COUNT -
#716
2f5a1c0Thanks @Anshumancanrock! - feat(nip42): add session tracking with optional TTL and publish-time authRequired (NIP-11 restricted_writes) -
#732
d413bd6Thanks @Anshumancanrock! - Add a CLI to mint NIP-43 invite codes (nostream invite create) so operators can issue a claim without SQL. New codes honornip43.defaultMaxUsesandnip43.inviteCodeExpirySeconds. -
#650
3461dfeThanks @Anshumancanrock! - Add NIP-43 invite code foundation: InviteCodeRepository with atomic claimCode, invite_codes migration, and event kind/tag constants. -
#738
a0853dbThanks @Anshumancanrock! - feat(nip43): issue kind 28935 invite codes on requestNIP-43 kind 28935 is not an event clients publish — it is a REQ the relay answers by
minting an invite code on the fly and returning a relay-signed ephemeral event. Nostream
now serves those subscriptions, completing the membership flow: request a claim, join with
kind 28934, publish.Off by default. It requires
nip43.enabledand the newnip43.allowInviteRequests, a
NIP-42 authenticated requester, aninfo.selfconsistent with the relay signing key, and a
per-pubkey budget under the newlimits.invite.rateLimits(5/hour by default). This also
makes the previously inertnip43.inviteRequestWhitelistsetting take effect. The minted
event is never persisted and never broadcast: the claim tag is a bearer secret and is sent
only to the socket that asked for it.Two fixes the flow depended on. The relay signs its own events with a key derived from
SECRET, butinfo.selfwas a hand-edited string that nothing validated — by default it
was a placeholder that is not a pubkey at all, so any NIP-43 client verifying a relay-signed
event againstselfwould reject it.info.selfis now optional: when unset or unparseable,
NIP-11 advertises the derived signing pubkey instead, andnostream infoprints that pubkey
so operators can pin it.Kind 28935 also sits in the ephemeral range, so a client-published one fell through to
EphemeralEventStrategyand was broadcast to every subscriber — including everyone
subscribed to kind 28935 waiting for a real invite. Anyone could inject a forgedclaimtag
into that subscription. It is now rejected with anOKfalse and never broadcast, and
bypasses the NIP-43 admission gate so that rejection actually reaches non-members, who are
the ones most likely to publish it by mistake while trying to obtain a code.CLI.md and README.md now describe the request flow. CLI.md previously claimed the relay
"does not yet generate kind 28935 onREQ", and never mentioned thatnostream info
prints the signing pubkey that CONFIGURATION.md tells operators to pin. -
#676
0bfa0b5Thanks @Anshumancanrock! - Add NIP-43 join/leave request event strategies (kinds 28934/28936) with NIP-42 auth enforcement, created_at freshness validation, invite code claiming, and admission management. Whennip43.enabledis set, publishing is restricted to admitted members even without payments enabled, and NIP-43 is advertised in the NIP-11 document (hidden when disabled). Join/leave update the admission cache so membership changes take effect immediately. -
#675
5a70839Thanks @Ferryx349! - feat(nip66): add shared relay probe engine for DNS, TLS, WebSocket RTT, and NIP-11 checks -
#741
afa8999Thanks @Ferryx349! - feat(nip66): publish kind 30166 and 10166 relay health events after probe runsAfter each relay monitor probe run, sign and store NIP-66 relay discovery and monitor
announcement events using the configured monitor identity, bootstrap kind 0/10002 on
first run, and persist via the existing parameterized replaceable event path.Fixes #696
-
#724
d14b1e9Thanks @Ferryx349! - feat(nip66): add RelayMonitorWorker cluster worker and probe scheduler -
#689
e294a71Thanks @Ferryx349! - Add NIP-66 relay monitor settings foundation with defaults for probe interval, timeouts, targets, monitor identity, and DNS cache TTL. -
#644
2f6d773Thanks @Anshumancanrock! - feat: reject NIP-70 protected events and reposts embedding them -
#730
ef4123eThanks @Anshumancanrock! - feat(admin): accept NIP-98 Authorization on protected admin API routes -
#722
cf5ea4fThanks @Anshumancanrock! - feat(nip98): add Authorization header event verifier for HTTP auth (kind 27235) -
#781
8d29f15Thanks @Ferryx349! - feat(admin): operator notification backend with Postgres outboxAdds transactional outbox dispatch for operator alerts (HTTP, Discord, Slack, Telegram), delivery log,
admin.notificationssettings, event hooks for admission invoices and settings changes, and admin test/history endpoints. Closes #759. -
#763
358763aThanks @Ferryx349! - feat(ops): add /readyz readiness probe for Postgres and RedisAdds a public readiness endpoint for zero-downtime deploy workflows. HAProxy (or similar) can use
/readyzto confirm an instance can serve traffic before cutover, while/healthzremains a lightweight liveness check. -
#672
595c0a6Thanks @Ferryx349! - refactor: extract shared settings-config module and guided schema for admin settings editor foundation -
#779
f6ee3e3Thanks @Priyanshubhartistm! - feat: wire the WoT graph into adaptive PoW difficultyAdds
limits.event.pow.wotThresholds, letting operators reduce (or bypass) the eventId PoW
requirement for pubkeys within their configured WoT distance. A direct follow can post instantly
under load while an unknown pubkey pays the full adaptive difficulty. Disabled by default (no
thresholds configured); requireswot.enabledto have any effect, since a pubkey's distance is
otherwise always unknown. -
#745
276fe16Thanks @Priyanshubhartistm! - feat: add a Web of Trust graph service that tracks NIP-02 follow distance from an operator-configured seed pubkeyAdds a
WotGraphServicethat builds a trust graph rooted atwot.seedPubkey, updated in real
time as kind-3 contact list events are ingested, with configurable depth (wot.maxDepth) and a
minimum-followers threshold for 2+ hop trust (wot.minimumFollowers). ExposesgetDistance()and
isTrusted()for other parts of the relay to query. Disabled by default (wot.enabled: false). -
#773
5577070Thanks @Ferryx349! - feat(shutdown): drain WebSocket clients on SIGTERMOn SIGTERM,
/readyzreturns 503, new WebSocket connections are rejected, and existing clients receive Nostr CLOSED messages before the socket closes. Drain is bounded byWS_DRAIN_TIMEOUT_MS(default 30s).
Patch Changes
-
#714
df1ed5dThanks @Ferryx349! - fix(admin): update aria-expanded and label when mobile menu is toggled -
#680
f92eabeThanks @Priyanshubhartistm! - fix: advertise NIP-13 (Proof of Work) support insupportedNips -
#750
f5c8dc8Thanks @Anshumancanrock! - deploy: run production migrations from the relay imageBake
migrations/andknexfile.jsinto the runtime image and point the
prod compose migrate service at that image so schema cannot drift from the
code that ships. -
#743
f36410aThanks @Ferryx349! - ci: build and push container image to GHCR after CI passes on mainAdds a GitHub Actions workflow that publishes
ghcr.io/cameri/nostream:mainand a
per-commitsha-*tag once the CI Checks workflow succeeds for pushes tomain. -
#775
59b1e51Thanks @Anshumancanrock! - fix: return COUNT results for filters with generic tag queries (#e,#p,#g,#h), which projectedevent_idtwice and failed with "error: unable to count events" -
#646
eb64d8aThanks @dependabot! - chore(deps): bump js-yaml from 4.1.1 to 4.2.0 -
#647
68da3d4Thanks @dependabot! - chore(deps): bump ws from 8.20.1 to 8.21.0 -
#705
95a672eThanks @dependabot! - chore(deps): bump axios from 1.16.0 to 1.18.0 -
#751
8509aeaThanks @Ferryx349! - deploy: minimal server bootstrap with optional settings overridesAdd deploy/bootstrap.sh, document what operators must keep locally vs what
ships in the image, and stop seeding a full settings.yaml on first boot so
release defaults merge with optional overrides only. -
#744
8fa72d6Thanks @Ferryx349! - deploy: add production Docker Compose stack for relay serversAdds a minimal prod compose file, migrate image Dockerfile, and server layout docs
for deployments that pullghcr.io/cameri/nostream:maininstead of building on the host. -
#765
3690bd8Thanks @Ferryx349! - ci: disable CodeQL workflowRemoves the CodeQL Advanced GitHub Actions workflow, custom query pack, config,
and route suppression comments to stop false-positive security alerts on admin
routes that already use custom auth and rate limiting. -
#694
7c4b728Thanks @Priyanshubhartistm! - fix: de-duplicate events returned by generic tag-filter subscriptionsEventRepository.findByFilters()left-joinsevent_tagsfor generic tag filters
(#e,#p, etc.) without deduplicating the result. An event matching more than one
tag row for the same filter (e.g.{"#p": ["a", "b"]}matching an event tagged with
both) was returned once per matchingevent_tagsrow, so subscribers received the
sameEVENTmessage multiple times. The query now selectsDISTINCT events.*for
tag-filtered queries so each stored event is returned at most once. This also covers
generic tag filters combined with a NIP-50searchterm (e.g.
{"search": "...", "#p": ["a", "b"]}), which take the search branch and are now
de-duplicated as well. -
#771
b07f9c3Thanks @chappie-daemon! - fix: enforcelimits.client.subscription.maxFilterValueson REQ and COUNT filtersFilters whose array criteria (
ids,authors,kinds,#<tag>) hold more than
maxFilterValuesvalues in total are now rejected withToo many filter values
instead of being handed to PostgreSQL as an unboundedWHERE IN (...). The
limit was previously defined in settings and surfaced in the admin settings
editor while being read by nothing at all.The enforced limit is also advertised in the NIP-11
limitationobject as
max_filter_values, a non-standard extension since NIP-11 has no field for
per-filter value counts. -
#703
7af0387Thanks @Priyanshubhartistm! - Fix the Content-Security-Policyconnect-srcdirective for relays served over plainws://.The web app factory derived an HTTP(S) origin from the relay's WebSocket URL but mapped
ws:to the invalid scheme':', which the WHATWG URL API silently ignores. As a result the
connect-srcdirective kept aws://…entry instead of the intendedhttp://…origin for
local/dev, Tor, or reverse-proxied setups. Thews:protocol now correctly maps tohttp:.Adds regression test coverage for the protocol mapping (
getWebProtocolForRelay, extracted from
createWebAppso it can be unit tested directly), since this file previously had no test coverage
at all. -
#708
a76d0b5Thanks @Priyanshubhartistm! - fix: reject expiration timestamp 0 and millisecond-scale values, and accept safe-integer second-based timestamps up to the Postgres int4 max (2038-01-19T03:14:07Z) in getEventExpiration() -
#747
14cca60Thanks @Ferryx349! - fix(ci): publish container image after CI passes on mainRemoves the standalone publish workflow and its unsafe workflow_run trigger.
Image publish now runs as the final job in checks.yml on pushes to main, after
lint, build, and tests succeed. -
#735
f0aab15Thanks @Ferryx349! - fix: include event id in expired and rate-limited rejection logsExpired and rate-limited event rejections logged
event %s rejected: ...without
passingevent.id, so operators saw a literal%sinstead of the event id. -
#715
9361601Thanks @Priyanshubhartistm! - fix: abort in-flight streaming queries when a subscription is cancelled -
#746
7297b96Thanks @Anshumancanrock! - fix(payments): stop stale invoices from wedging payment pollingA relay could stop clearing payments entirely, needing
delete from invoicesto
recover. Two things combined to cause it.Invoices created without an expiry could never be retired, because the expiry
check treats a missing date as "not expired", so the maintenance worker left them
pending forever. The LNURL processor set no expiry on any invoice, making this
certain there rather than incidental. Invoices now fall back to
payments.invoiceExpirySecondswhen the processor reports no expiry of its own,
and existing pending rows without one are backfilled.Separately, each maintenance pass re-read the same oldest page of pending
invoices, so one page of invoices that never resolve starved every newer one
indefinitely. The worker now advances through the queue and wraps at the end,
keeping the same per-pass cost while guaranteeing every pending invoice is
eventually polled. -
#786
e87e1afThanks @Priyanshubhartistm! - fix(nip56): skip targetless report rows, record every p/e target, batch report inserts in one transaction, and warm the WoT graph at boot instead of blocking the first report on a cold-start rebuild -
#733
9fb1c10Thanks @Ferryx349! - test(nip66): add integration tests for RelayMonitorWorker snapshot storage -
#725
849c3f7Thanks @Anshumancanrock! - feat(http): build absolute request URL from relay_url -
#726
3d3848eThanks @Anshumancanrock! - feat(redis): add setKeyIfNotExists for one-time claims -
#640
ca23be1Thanks @Anshumancanrock! - test: optimize nip05.spec.ts & nip03.spec.ts resource management- Lift sinon stub to
before/afterin verifyNip05Identifier tests (create once, reset between tests) - Extract SSRF guard callback once in
beforeinstead of per-testbeforeEach - Pre-build shared OTS buffers and attestations at module scope to eliminate redundant Buffer.concat calls
- Add shared event factory for extractNip05FromEvent tests
- Lift sinon stub to
-
#686
cb7daf6Thanks @Priyanshubhartistm! - fix: stop checking additional rate limit windows once a client is already rate-limitedisRateLimited()inEventMessageHandlerandWebSocketAdapterlooped through every
configured rate limit window even after one had already tripped, callingrateLimiter.hit()
(a Redis write) for each remaining window. Both now return as soon as the first exceeded
window is found, avoiding redundant Redis writes for clients that are already being limited. -
#684
3648954Thanks @Priyanshubhartistm! - fix: await Redis EXISTS call in RedisAdapter.hasKey() so it reflects actual key presence instead of always returning true -
#711
220949dThanks @Priyanshubhartistm! - fix: include the actual error message in replaceable event rejection responsesReplaceableEventStrategy.execute()sent clients a bareerror:command result
(with no message body) whenevereventRepository.upsert()failed for a reason other
than a duplicate event id. The underlyingerror.messagewas caught but never
included in the response, leaving clients with no actionable information about why
the event was rejected. The command result now includeserror.message. -
#682
dc78df5Thanks @Priyanshubhartistm! - fix: prevent crash in NIP-11 relay information document when payments settings are absent