Skip to content

Add nip05_whitelist filter - #19

Open
Michilis wants to merge 2 commits into
damus-io:masterfrom
Michilis:Nip-05-whitelist
Open

Michilis wants to merge 2 commits into
damus-io:masterfrom
Michilis:Nip-05-whitelist

Conversation

@Michilis

@Michilis Michilis commented Apr 17, 2026

Copy link
Copy Markdown

New filter that loads pubkeys from a NIP-05 nostr.json file (local path or HTTP/HTTPS URL) into an in-memory HashSet and rejects notes from pubkeys not in the set. The whitelist is refreshed in a background thread on a configurable interval (default 60s), with failed reloads logging an error and preserving the previous set. Uses ureq for lightweight sync HTTP fetching.

Summary by CodeRabbit

  • New Features

    • Added a NIP-05 whitelist filter that validates incoming notes against pubkeys loaded from a URL or local JSON and supports configurable rejection messages
    • Supports automatic whitelist reloading at configurable intervals
  • Documentation

    • Added commented configuration example showing how to enable and configure the NIP-05 whitelist filter
  • Chores

    • Added an external HTTP client dependency

New filter that loads pubkeys from a NIP-05 nostr.json file (local path
or HTTP/HTTPS URL) into an in-memory HashSet and rejects notes from
pubkeys not in the set. The whitelist is refreshed in a background thread
on a configurable interval (default 60s), with failed reloads logging an
error and preserving the previous set. Uses ureq for lightweight sync
HTTP fetching.
@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a new NIP-05 whitelist note filter that loads allowed pubkeys from a configured JSON source (HTTP(S) URL or local file), caches them, spawns a periodic reload thread, and filters incoming notes by pubkey membership with a configurable rejection message.

Changes

Cohort / File(s) Summary
Dependency
Cargo.toml
Added ureq = "2" HTTP client dependency.
Configuration
noteguard.toml
Added commented instructions and an example nip05_whitelist filter configuration (source, reload_interval_secs, message) — remains commented/out-of-band unless enabled.
New Filter Implementation
src/filters/nip05_whitelist.rs
Introduced Nip05Whitelist NoteFilter: loads/parses NIP-05 JSON names map (max 10 MiB), stores pubkeys in an Arc<RwLock<HashSet<String>>>, lazy-initializes on first use, spawns a background reload loop to refresh the set, and accepts/rejects notes based on pubkey membership with configurable message.
Module export
src/filters/mod.rs
Re-exported Nip05Whitelist (pub use nip05_whitelist::Nip05Whitelist;).
Registration
src/main.rs
Registered Nip05Whitelist among builtin filters in Noteguard::register_builtin_filters.

Sequence Diagram(s)

sequenceDiagram
    participant Noteguard as Noteguard System
    participant Filter as Nip05Whitelist Filter
    participant Source as JSON Source (URL or File)
    participant Thread as Reload Thread
    participant Notes as Incoming Notes

    Noteguard->>Filter: first initialization (on first note)
    activate Filter
    Filter->>Source: fetch NIP-05 JSON (names map)
    Source-->>Filter: JSON payload
    Filter->>Filter: parse & populate HashSet (RwLock)
    Filter-->>Noteguard: init complete (log)
    deactivate Filter

    loop periodic reload (reload_interval_secs)
        Thread->>Source: refetch JSON
        Source-->>Thread: JSON payload
        Thread->>Filter: atomically replace HashSet (RwLock)
        Thread-->>Thread: log success/error
    end

    loop incoming notes
        Notes->>Filter: deliver note
        Filter->>Filter: check msg.event.pubkey in HashSet
        alt pubkey present
            Filter-->>Notes: accept
        else pubkey absent
            Filter-->>Notes: reject (custom/default message)
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I fetched the list from far and near,
Kept keys in burrows safe and clear.
A thread wakes up to check the gate,
Only trusted hoppers get to skate.
Hop, reload, protect — whitelist is here!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add nip05_whitelist filter' directly and concisely describes the main change: introducing a new NIP-05 whitelist filter. It matches the primary objective of the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/main.rs (1)

1-3: Add the new builtin to the registration test.

The filter is registered correctly, but test_register_builtin_filters still does not assert "nip05_whitelist", so this wire-up can regress silently.

✅ Proposed test assertion
         assert!(noteguard.registered_filters.contains_key("kinds"));
+        assert!(noteguard
+            .registered_filters
+            .contains_key("nip05_whitelist"));

Also applies to: 56-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 1 - 3, The test test_register_builtin_filters is
missing an assertion for the newly-registered filter "nip05_whitelist", so
registration can regress silently; update test_register_builtin_filters to
include an assertion that the registered builtin filter names (or the list it
inspects) contains "nip05_whitelist" alongside the existing entries (e.g.,
assert presence in whatever collection is returned by the registration
function), referencing the test function name test_register_builtin_filters and
the filter identifier "nip05_whitelist" to ensure the new builtin is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@noteguard.toml`:
- Around line 26-29: The nip05_whitelist filter block (filters.nip05_whitelist)
is inert unless the top-level pipeline includes "nip05_whitelist"; add a
commented example entry to the pipeline configuration showing how to enable it
(e.g., add "nip05_whitelist" to the pipeline array or sequence), so users know
they must include the filter name in the pipeline to activate
filters.nip05_whitelist.

In `@src/filters/nip05_whitelist.rs`:
- Around line 88-89: The reload interval must be clamped to avoid a zero-second
tight loop: when building the Duration for spawn_reload_thread, check
self.reload_interval_secs (and its unwrap_or default) and ensure it is at least
1 (or another sane minimum) before calling Duration::from_secs; update the code
around the Duration::from_secs(self.reload_interval_secs.unwrap_or(60)) so it
uses something like max(1, value) or returns an early error/uses a safe default,
and then call spawn_reload_thread(self.source.clone(), interval, pubkeys) with
the clamped interval to prevent a busy loop.
- Around line 29-38: The current read logic for `body` can block or OOM on
slow/large HTTP responses; change the HTTP path to use a ureq::Agent with
explicit connect/read timeouts (e.g.,
Agent::new().timeout_read(...).timeout_connect(...)) and fetch via
agent.get(source).call()?.into_reader(), then read into a buffer but limit bytes
using std::io::Read::take with a MAX_BYTES constant and return an error if more
data exists; similarly replace std::fs::read_to_string(source) with opening a
File and reading via file.take(MAX_BYTES).read_to_end(...) (or read_to_string on
the taken reader) so local files are also capped, and keep the same error
mapping messages; reference the existing `is_url(source)` check, the `body`
variable, and introduce a MAX_BYTES constant near this code.

---

Nitpick comments:
In `@src/main.rs`:
- Around line 1-3: The test test_register_builtin_filters is missing an
assertion for the newly-registered filter "nip05_whitelist", so registration can
regress silently; update test_register_builtin_filters to include an assertion
that the registered builtin filter names (or the list it inspects) contains
"nip05_whitelist" alongside the existing entries (e.g., assert presence in
whatever collection is returned by the registration function), referencing the
test function name test_register_builtin_filters and the filter identifier
"nip05_whitelist" to ensure the new builtin is covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 097037a6-6133-4989-b48e-0bc7276dc6a5

📥 Commits

Reviewing files that changed from the base of the PR and between 4d4d9d6 and 2fb79e4.

📒 Files selected for processing (5)
  • Cargo.toml
  • noteguard.toml
  • src/filters/mod.rs
  • src/filters/nip05_whitelist.rs
  • src/main.rs

Comment thread noteguard.toml
Comment thread src/filters/nip05_whitelist.rs
Comment thread src/filters/nip05_whitelist.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/filters/nip05_whitelist.rs`:
- Around line 66-76: The code is logging raw `source` (which may contain
secrets); create and use a sanitizer (e.g., `redact_source` or
`sanitize_source`) that strips URL userinfo (username:password@) and replaces
sensitive filesystem paths with a safe placeholder or basename, then pass the
sanitized value into all error/log messages and calls that only need a display
string; update the `map_err` closures around `agent.get(source).call()` and
`std::fs::File::open(source)` and change the second argument to
`read_capped(reader, sanitized, "...")` / `read_capped(file, sanitized, "...")`
(keep the original `source` for actual I/O but use `sanitized` for any
logging/display).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 38fe8f1a-4c2d-47f4-b447-727164d11136

📥 Commits

Reviewing files that changed from the base of the PR and between 2fb79e4 and 740361a.

📒 Files selected for processing (2)
  • noteguard.toml
  • src/filters/nip05_whitelist.rs
✅ Files skipped from review due to trivial changes (1)
  • noteguard.toml

Comment on lines +66 to +76
let reader = agent
.get(source)
.call()
.map_err(|e| format!("HTTP request failed for {}: {}", source, e))?
.into_reader();

read_capped(reader, source, "response body from")?
} else {
let file = std::fs::File::open(source)
.map_err(|e| format!("failed to read file {}: {}", source, e))?;
read_capped(file, source, "file")?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redact source before logging it.

source can be an authenticated URL or an internal filesystem path, and the current success/error logs emit it verbatim. That leaks secrets and deployment details into logs; log a sanitized display value instead.

🛡️ Suggested direction
+use std::path::Path;
+
+fn source_for_log(source: &str) -> String {
+    if is_url(source) {
+        let without_query = source.split('?').next().unwrap_or(source);
+        match without_query.rsplit_once('@') {
+            Some((prefix, tail)) if prefix.contains("://") => {
+                let scheme = prefix.split("://").next().unwrap_or("https");
+                format!("{scheme}://{tail}")
+            }
+            _ => without_query.to_string(),
+        }
+    } else {
+        Path::new(source)
+            .file_name()
+            .and_then(|name| name.to_str())
+            .unwrap_or("<local file>")
+            .to_string()
+    }
+}
+
 fn fetch_pubkeys(source: &str) -> Result<HashSet<String>, String> {
+    let log_source = source_for_log(source);
     let body = if is_url(source) {
         let agent = ureq::AgentBuilder::new()
             .timeout_connect(HTTP_CONNECT_TIMEOUT)
             .timeout_read(HTTP_READ_TIMEOUT)
             .build();
 
         let reader = agent
             .get(source)
             .call()
-            .map_err(|e| format!("HTTP request failed for {}: {}", source, e))?
+            .map_err(|e| format!("HTTP request failed for {}: {}", log_source, e))?
             .into_reader();
 
-        read_capped(reader, source, "response body from")?
+        read_capped(reader, &log_source, "response body from")?
     } else {
         let file = std::fs::File::open(source)
-            .map_err(|e| format!("failed to read file {}: {}", source, e))?;
-        read_capped(file, source, "file")?
+            .map_err(|e| format!("failed to read file {}: {}", log_source, e))?;
+        read_capped(file, &log_source, "file")?
     };
@@
                 info!(
                     "nip05_whitelist: reloaded {} pubkeys from {}",
-                    count, source
+                    count, source_for_log(&source)
                 );
@@
                 info!(
                     "nip05_whitelist: loaded {} pubkeys from {}",
                     set.len(),
-                    self.source
+                    source_for_log(&self.source)
                 );

Also applies to: 95-101, 109-119

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/filters/nip05_whitelist.rs` around lines 66 - 76, The code is logging raw
`source` (which may contain secrets); create and use a sanitizer (e.g.,
`redact_source` or `sanitize_source`) that strips URL userinfo
(username:password@) and replaces sensitive filesystem paths with a safe
placeholder or basename, then pass the sanitized value into all error/log
messages and calls that only need a display string; update the `map_err`
closures around `agent.get(source).call()` and `std::fs::File::open(source)` and
change the second argument to `read_capped(reader, sanitized, "...")` /
`read_capped(file, sanitized, "...")` (keep the original `source` for actual I/O
but use `sanitized` for any logging/display).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant