Repository navigation
Conversation
Closes #634. Adds admin API and Moderation console for pubkey/kind lists, rate-limit bypass, ban/unban actions, and publisher/IP/rate-limit stats.
🦋 Changeset detectedLatest commit: fddff7f The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
[Medium impact] The changes affect relay event moderation within the authenticated admin workflow. Concurrent moderation saves can silently undo bans, so this needs a fix before merging. Findings
|
Coverage Report for CI Build 40060142350Warning No base build found for commit Coverage: 75.733%Details
Uncovered Changes
Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats💛 - Coveralls |
Keep pubkey whitelist on ban; use daily Redis buckets for rate-limit hits; isolate moderation tests with temp NOSTR_CONFIG_DIR; skip stats when cache has no eval.
Use ZREVRANGE with the requested limit instead of scanning full daily sets.
Union day buckets in Redis, then return top N so multi-day totals stay correct.
Drop the temporary ZUNIONSTORE destination immediately so stats refresh does not retain duplicate daily bucket copies in Redis.
Improve coverage for moderation limits, actions, stats, and rate-limit helpers.
Use callCount in rate-limit stats tests, cover query helpers and factory wiring, and add error-path cases for moderation actions and stats.
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
…dows Deep-merge defaults before validateSettings so partial info overrides work, and include every UTC day bucket touched by the requested stats window.
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
| const userSettings = loadUserSettings() as Settings | ||
| const nextUserSettings = applyLimitsToUserSettings(userSettings, normalized) | ||
| const mergedNext = mergeDeepRight(loadDefaults(), nextUserSettings) as Settings | ||
|
|
||
| const issues = validateSettings(mergedNext) | ||
| if (issues.length > 0) { | ||
| throw new ValidationError('Validation failed', issues) | ||
| } | ||
|
|
||
| saveSettings(nextUserSettings) |
There was a problem hiding this comment.
On relays with multiple workers, two moderation actions can overwrite each other. Each request reads every list, changes its copy, then replaces all lists without a shared lock. If two workers read before either saves, banning different pubkeys leaves only the last ban, even though both requests succeed.
Lock the full read-change-save operation across workers and read the current lists inside that lock.
| .select(db.raw(`encode(event_pubkey, 'hex') as pubkey`)) | ||
| .count('* as eventCount') | ||
| .whereNull('deleted_at') | ||
| .where('event_created_at', '>=', sinceUnixSeconds) |
There was a problem hiding this comment.
Recent uploads miss activity totals
Both activity queries filter by event_created_at, which copies the publisher's created_at, rather than when the relay received the event. With the default maxNegativeDelta: 0, clients can upload older events today without appearing in today's activity totals. Admins can therefore miss recent heavy publishers and client IPs.
Use first_seen for the receipt window in both queries.
| case 'whitelist-ip': { | ||
| const ip = fields.ip?.trim() | ||
| if (!ip || isIP(ip) === 0) { | ||
| throw new Error('ip is required and must be a valid IPv4 or IPv6 address') | ||
| } | ||
| return { | ||
| ...current, | ||
| rateLimitBypass: { | ||
| ...current.rateLimitBypass, | ||
| ipAddresses: [...new Set([...current.rateLimitBypass.ipAddresses, ip])], |
There was a problem hiding this comment.
Valid IPv6 bypasses do nothing
IP bypass actions accept any valid IPv6 spelling but save and remove it as an exact string. The socket uses SocketAddress, which returns a standard spelling, while the rate-limit check uses includes. Entering 2001:0db8:0:0:0:0:0:1 succeeds but does not match the client's 2001:db8::1. Another valid spelling can also fail to remove an existing bypass.
Normalize addresses the same way before adding, removing, and comparing them.
Description
This PR adds Admin Console Moderation :- view/update pubkey & kind lists and rate-limit bypass lists, quick ban/unban and IP bypass actions, and 7-day stats (top publishers, client IPs, rate-limit hits). Persists limit changes to settings with audit logging.
Related Issue
Closes #634
Motivation and Context
How Has This Been Tested?
Screenshots (if appropriate):
Screen.Recording.2026-10-23.at.13.38.14.mov
Types of changes
Checklist: