Merge pull request #6 from PR0M3TH3AN/codex/improve-web-of-trust-documentation-plan

docs: strengthen web-of-trust feature development plan
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-05-16 12:59:04 -04:00
commit 0972d2d4fa
1 file changed
+182 -181
+182 -181
View File
@@ -1,160 +1,152 @@
# Trust & Block Feature Plan
# Trust, Mute, and Web-of-Trust (WoT) Plan
## Overview
Add web-of-trust filtering and user block lists to the community template feed so users can
hide content from bad actors and prioritize templates from trusted sources. Because the app
already uses Nostr for identity and publishing, this feature can be built entirely on existing
Nostr protocol standards (NIPs) with no backend required.
## Goals
Build an **abuse-resistant community feed** that still feels open and discoverable, using only Nostr-native data and client-side logic.
## Complexity
**Moderate–High** — Estimated 4–8 hours to implement (phased; Phase 1 alone is ~2 hours)
### Product goals
1. Reduce spam and low-quality content quickly.
2. Keep control in users’ hands (local + portable moderation).
3. Preserve feed performance for both logged-out and logged-in users.
4. Ship in small phases with measurable outcomes.
### Non-goals (v1)
- Centralized server moderation.
- Perfect Sybil resistance.
- Cross-relay consensus on reports.
---
## Background: Nostr Primitives Available
| NIP | Kind | Purpose |
|-----|------|---------|
| NIP-02 | 3 | Contact / follow list — the source of truth for the social graph |
| NIP-51 | 10000 | Mute list — pubkeys, event IDs, keywords to suppress |
| NIP-56 | 1984 | Content report — flag a pubkey or event as spam/NSFW/illegal |
These events are already signed with the user's key (via NIP-07 extension) and stored on
Nostr relays, so the block/mute list automatically syncs across any Nostr-compatible client
the user signs in to.
## Current architecture assumptions
- Identity and signing via **NIP-07** browser extension.
- Community templates are already rendered from Nostr events.
- App can query multiple relays and currently has a feed render pipeline.
---
## Phased Plan
## Key Nostr primitives
### Phase 1 — Local Block List (no login required)
The simplest layer: any visitor can block a pubkey locally without signing in.
#### Storage
- `localStorage` key: `casewrap-blocked-pubkeys` → JSON array of hex pubkeys
- Survives page refresh; cleared only if the user explicitly unblocks or clears browser data
#### UI
- Add a "Block this author" option to each template card menu (⋮ or right-click)
- Blocked templates are hidden from the feed immediately (no reload needed)
- Settings panel shows blocked pubkey list with per-entry "Unblock" buttons
- Blocked count shown as a subtle badge: "3 authors hidden"
#### Core Functions
```javascript
function blockPubkey(pubkey)
// Add to localStorage block list, re-filter visible templates
function unblockPubkey(pubkey)
// Remove from localStorage block list, re-filter
function isBlocked(pubkey)
// Returns true if pubkey is in the local block list
function applyBlockFilter(templates)
// Filter template array, removing any whose author is blocked
```
| NIP | Kind | Usage in this feature |
|-----|------|------------------------|
| NIP-02 | `3` | Follow graph (hop-1, optional hop-2) |
| NIP-51 | `10000` | User mute list (`p` tags, optional words/events) |
| NIP-56 | `1984` | Abuse reports (advisory to relays/tools) |
---
### Phase 2 — Nostr Mute List Sync (requires NIP-07 login)
## Phased delivery plan
Upgrade the local block list to a Nostr-native mute list (NIP-51 kind 10000) so it persists
across devices and syncs with other Nostr apps (Damus, Primal, Snort, etc.).
## Phase 1 — Local block list (works logged out)
#### On Login
1. Fetch the user's existing kind 10000 mute list from relays
2. Merge with any locally blocked pubkeys (union, no duplicates)
3. Save merged list back to the relay (signed via `window.nostr.signEvent`)
### Why first
Immediate abuse relief, zero protocol dependencies, and easy rollback.
#### On Block
1. Add pubkey to the in-memory mute list
2. Publish updated kind 10000 event to relays
3. Update `localStorage` as a local cache
### Data model
- `localStorage['casewrap:block:list:v1'] = string[]` (hex pubkeys)
- Normalize pubkeys to lowercase hex before insert.
- Keep a companion metadata object for analytics/debug (optional):
- `localStorage['casewrap:block:meta:v1'] = { [pubkey]: { createdAt, source } }`
#### Event Schema
### UX
- Card menu action: **Block author**.
- Settings section: list blocked pubkeys + unblock button.
- Inline feed toast: “Author blocked. Undo”.
- Badge in settings: “N authors hidden”.
### Engineering notes
- Block filter is applied as the first feed filter stage.
- Use a `Set` in memory to avoid repeated array scans.
- Add defensive guard for malformed pubkeys.
### Acceptance criteria
- Blocking hides author content immediately in all feed views.
- Unblock restores content without full refresh.
- Block state persists across refresh.
---
## Phase 2 — Mute list sync (NIP-51)
### Why second
Makes moderation portable across devices and compatible with other Nostr clients.
### Sync strategy
On successful NIP-07 login:
1. Fetch latest kind `10000` authored by viewer pubkey.
2. Parse `p` tags into `remoteMuted` set.
3. Read local block set `localMuted`.
4. Merge union -> `effectiveMuted`.
5. If union differs from remote, publish updated kind `10000`.
6. Cache `effectiveMuted` locally.
### Conflict policy
- **Union wins** by default (safer for abuse prevention).
- Never silently remove remote mutes.
- If publish fails, keep local behavior and surface non-blocking warning.
### Event shape
```json
{
"kind": 10000,
"tags": [
["p", "<hex-pubkey>"],
["p", "<hex-pubkey>"]
],
"tags": [["p", "<hex-pubkey>"]],
"content": ""
}
```
#### Core Functions
```javascript
async function fetchMuteList(pubkey)
// Query relays for kind 10000 authored by pubkey, return p-tag array
async function publishMuteList(mutedPubkeys)
// Sign and publish updated kind 10000 event
async function syncMuteListOnLogin()
// Merge relay mute list with localStorage block list on sign-in
```
### Acceptance criteria
- Existing remote mute list is respected on login.
- New block actions publish updated mute list.
- Local filter still works if relays/signing fail.
---
### Phase 3 — Web-of-Trust Feed Filter (requires NIP-07 login)
## Phase 3 — WoT-aware ranking and filtering
Only show templates from pubkeys within your social graph — the most effective spam defense.
### Why third
Highest feed-quality impact for signed-in users.
#### How It Works
1. Fetch the signed-in user's follow list (NIP-02 kind 3) → set of followed pubkeys ("hop 1")
2. Optionally fetch follows-of-follows → "hop 2" (wider but noisier)
3. In the community feed, templates are ranked/filtered by trust tier:
### Trust tiers
- **Blocked**: in effective mute set -> hidden.
- **Trusted (hop-1)**: directly followed by viewer.
- **Extended (hop-2)**: followed by someone viewer follows.
- **Unknown**: no graph evidence.
| Tier | Source | Display |
|------|--------|---------|
| Trusted | Pubkey is in your follow list (hop 1) | Full display, shown first |
| Extended | Pubkey is in follows-of-follows (hop 2) | Shown normally |
| Unknown | No social graph overlap | Shown with a "low trust" badge |
| Blocked | In mute list | Hidden entirely |
### Modes
- Default mode: **rank by trust**, don’t hard-hide unknown.
- Optional strict mode toggle: **show trusted only**.
- Optional toggle: enable/disable hop-2 expansion.
#### Trust Score (optional enhancement)
For each template author, count how many of your follows also follow them.
This score (0–N) can be shown as a small indicator on the template card.
### Ranking algorithm (v1)
Sort key tuple:
1. Tier priority (`trusted` > `extended` > `unknown`)
2. Optional social score (# of hop-1 followers who follow author)
3. Existing recency score (keep current feed freshness behavior)
```javascript
async function fetchFollowList(pubkey)
// Query relays for kind 3, return array of followed pubkeys
### Performance constraints
- Fetch hop-1 first; render immediately with partial trust state.
- Compute hop-2 only if user enables it.
- Cache graph in `sessionStorage` with TTL (e.g., 15 min).
- Hard cap hop-2 expansion fanout (e.g., max 500 hop-1 nodes queried).
- Use batched relay subscriptions; close on EOSE.
function buildTrustSet(myFollows, followsOfFollows)
// Returns { hop1: Set<pubkey>, hop2: Set<pubkey> }
function getTrustTier(authorPubkey, trustSet)
// Returns 'trusted' | 'extended' | 'unknown' | 'blocked'
function sortByTrust(templates, trustSet)
// Put hop-1 templates first, then hop-2, then unknown
```
#### Caching
- Follow lists are large — cache in `sessionStorage` keyed by pubkey
- Re-fetch at most once per session to avoid relay hammering
- Show a loading indicator while the graph is being fetched
#### UI Controls
- Toggle in settings: "Show only trusted authors" (default: off — non-breaking)
- Toggle: "Show low-trust badge on unknown authors" (default: on when logged in)
- Trust tier shown as a subtle icon on template cards (★ trusted, ~ extended, ? unknown)
### Acceptance criteria
- Trust tiers display correctly for sampled known authors.
- “Trusted only” mode hides extended + unknown.
- Feed first paint is not blocked by hop-2 queries.
---
### Phase 4 — Content Reporting (NIP-56)
## Phase 4 — Reporting (NIP-56)
Allow users to flag templates as spam, NSFW, or abusive so relay operators can act.
### Why fourth
Valuable ecosystem signal but lower immediate user impact than blocking/filtering.
#### Report Flow
1. User clicks "Report" in the template card menu
2. Modal asks for report type: `spam` | `nudity` | `illegal` | `impersonation`
3. App publishes a NIP-56 kind 1984 event referencing the template event ID and author pubkey
### UX
- Card menu -> **Report**.
- Reason required (`spam`, `nudity`, `illegal`, `impersonation`, `other`).
- Optional note.
- Post-submit option: “Also block this author”.
#### Event Schema
### Event shape
```json
{
"kind": 1984,
@@ -162,86 +154,95 @@ Allow users to flag templates as spam, NSFW, or abusive so relay operators can a
["e", "<template-event-id>", "spam"],
["p", "<author-pubkey>"]
],
"content": "Optional description"
"content": "Optional detail"
}
```
#### Notes
- Reports are published to relays but have no local effect by themselves
- Optionally: auto-hide locally after reporting (user already chose to flag it)
- Reports help relay operators and moderation tools (like Spam Scout) build block lists
```javascript
async function reportTemplate(eventId, authorPubkey, reason, description)
// Sign and publish NIP-56 kind 1984 event
```
### Acceptance criteria
- Signed event is published with valid target tags.
- Failures are surfaced to user; no silent drop.
---
## Integration Points
## Cross-cutting concerns
All four phases hook into the community feed render loop. The minimal change is:
### Security & abuse hardening
- Validate all incoming pubkeys/event ids before use.
- Debounce block/report UI actions to avoid duplicate publishes.
- Protect against relay poisoning by taking latest replaceable event only.
```javascript
// In the function that renders community template cards:
function renderCommunityFeed(templates) {
const filtered = applyBlockFilter(templates); // Phase 1
const sorted = sortByTrust(filtered, trustSet); // Phase 3
filtered.forEach(t => renderCard(t, getTrustTier(t.author, trustSet)));
}
```
### Privacy
- WoT computation occurs client-side.
- Do not log raw social graph by default.
- If telemetry is added, aggregate counts only.
Block/report actions live in the template card's action menu alongside existing
"Copy link" / "Import" options.
### Observability (recommended)
Track coarse metrics:
- `% feed hidden by block list`
- `% users enabling trusted-only`
- `median feed render time (logged in/out)`
- `mute sync success rate`
### Failure handling
- If NIP-07 unavailable: Phase 1 fully operational; hide sync/report controls.
- If relay read fails: preserve local cache and degrade gracefully.
- If relay publish fails: keep local state, show retry affordance.
---
## Performance Considerations
- Follow list fetches are async and cached — they don't block the initial feed render
- Local block filter runs synchronously on the already-fetched template array (~1ms)
- Follows-of-follows (hop 2) can be 10,000+ pubkeys — use a `Set` for O(1) lookups
- Only fetch hop-2 if the user enables the extended trust option (off by default)
- Relay queries use `REQ` filters with `kinds: [3]` — single-use subscriptions, close after `EOSE`
## Implementation slices (suggested task breakdown)
1. **Data layer**: block list store + normalization + helpers.
2. **Feed pipeline**: filter + tiering + ranking hooks.
3. **UI**: card menu actions, settings controls, trust badges.
4. **Nostr sync**: kind 10000 read/merge/publish.
5. **Reporting**: kind 1984 modal and publish flow.
6. **Instrumentation**: lightweight counters/timers.
---
## Limitations
- Web-of-trust only works when the user is signed in via NIP-07
- Users with small follow lists get little benefit from WoT filtering
- Relay queries for follows-of-follows can be slow on large social graphs (~5–15s)
- NIP-56 reports are advisory only — relay operators decide whether to act
- No cross-relay mute list deduplication (if user uses multiple relay sets)
## Revised testing plan
### Unit tests
- Pubkey normalization and dedupe logic.
- Union merge behavior for local vs remote mute lists.
- Trust-tier classifier and rank sort stability.
### Integration tests
- Logged-out: block/unblock lifecycle across refresh.
- Logged-in: mute sync read/merge/publish happy path.
- Logged-in degraded: relay failure + signer rejection handling.
- Trusted-only toggle behavior with mixed-tier fixture feed.
### Performance tests
- Feed render timing with:
- 0 blocks
- 500 blocked pubkeys
- hop-1 only
- hop-1 + capped hop-2
### Manual QA checklist
- [ ] Block author from card and confirm immediate hide.
- [ ] Undo block toast restores card.
- [ ] Login merges local + remote mute sets without loss.
- [ ] Trust badges are accurate for controlled fixture accounts.
- [ ] Trusted-only mode does not show unknown authors.
- [ ] Report flow publishes kind 1984 and handles failure visibly.
---
## Future Enhancements
- Subscribe to community-maintained block lists (NIP-51 kind 30000 "follow sets" used as shared deny lists)
- Import block list from Nostr clients (Damus, Primal) via relay sync
- NSFW blur overlay instead of full hide (user can click to reveal)
- Relay-level WoT filtering: connect to WoT-curated relays (e.g. relay.nostr.band WoT endpoint) for pre-filtered feeds
- Keyword/hashtag muting (NIP-51 supports `["word", "badterm"]` tags in kind 10000)
## Is the original plan good enough?
Short answer: **good foundation, but not quite “as good as it could be” for production**.
---
### What it already did well
- Correct NIP choices.
- Practical phased rollout.
- Good focus on no-backend architecture.
## Implementation Order (recommended)
### Biggest gaps this revision closes
- Explicit conflict/merge policy for mute sync.
- Concrete degradation behavior on relay/signing failure.
- Performance guardrails for hop-2 fanout.
- Better UX defaults (rank-first vs hide-first).
- Test strategy split by unit/integration/perf.
- Observability so you can prove impact after launch.
1. **Phase 1** — Local block list. Zero dependencies, works for logged-out users, shippable alone.
2. **Phase 3** — WoT feed sort/filter. High value for logged-in users; Phase 2 not required first.
3. **Phase 2** — Nostr mute list sync. Upgrades Phase 1 to be cross-device.
4. **Phase 4** — Reporting. Lowest urgency; depends on relay operator buy-in.
---
## Testing Checklist
- [ ] Blocking a pubkey hides all their templates immediately
- [ ] Unblocking restores their templates
- [ ] Block list persists across page refresh (localStorage)
- [ ] Blocked pubkeys are excluded from all feed views (not just main)
- [ ] Mute list sync fetches existing kind 10000 on login (Phase 2)
- [ ] Publishing updated mute list signs correctly via NIP-07 (Phase 2)
- [ ] Follow list fetch correctly identifies hop-1 pubkeys (Phase 3)
- [ ] Trust tier badges display correctly on template cards (Phase 3)
- [ ] "Show only trusted" toggle hides unknown-tier templates (Phase 3)
- [ ] Report publishes a valid kind 1984 event to relays (Phase 4)
- [ ] No performance regression on feed load (follow list fetched async)
- [ ] Logged-out users see Phase 1 block UI only (no WoT controls)