12 changed files with 224 additions and 20 deletions
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest' |
||||
import { |
||||
eventSeenOnMatchesAllowlist, |
||||
filterRelaysToUserAllowlist, |
||||
isRelayInUserAllowlist |
||||
} from '@/lib/relay-allowlist' |
||||
|
||||
describe('relay-allowlist', () => { |
||||
const allow = ['wss://theforest.nostr1.com/', 'wss://feeds.nostrarchives.com/notes/trending/reactions/today'] |
||||
|
||||
it('matches hostname across schemes', () => { |
||||
expect(isRelayInUserAllowlist('wss://theforest.nostr1.com/', allow)).toBe(true) |
||||
expect(isRelayInUserAllowlist('https://theforest.nostr1.com/', allow)).toBe(true) |
||||
expect(isRelayInUserAllowlist('wss://nostr.wine/', allow)).toBe(false) |
||||
}) |
||||
|
||||
it('filters relay lists to the allowlist', () => { |
||||
expect( |
||||
filterRelaysToUserAllowlist( |
||||
['wss://nostr.wine/', 'wss://theforest.nostr1.com/', 'wss://theforest.nostr1.com/'], |
||||
allow |
||||
) |
||||
).toEqual(['wss://theforest.nostr1.com/']) |
||||
}) |
||||
|
||||
it('eventSeenOnMatchesAllowlist allows empty seen-on and blocks unknown relays', () => { |
||||
expect(eventSeenOnMatchesAllowlist([], allow)).toBe(true) |
||||
expect(eventSeenOnMatchesAllowlist(['wss://theforest.nostr1.com/'], allow)).toBe(true) |
||||
expect(eventSeenOnMatchesAllowlist(['wss://nostr.wine/'], allow)).toBe(false) |
||||
expect( |
||||
eventSeenOnMatchesAllowlist(['wss://nostr.wine/', 'wss://theforest.nostr1.com/'], allow) |
||||
).toBe(true) |
||||
}) |
||||
}) |
||||
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
import { normalizeAnyRelayUrl } from '@/lib/url' |
||||
|
||||
function relayHostname(url: string): string | null { |
||||
const normalized = normalizeAnyRelayUrl(url) || url.trim() |
||||
if (!normalized) return null |
||||
try { |
||||
return new URL(normalized).hostname.toLowerCase() |
||||
} catch { |
||||
return null |
||||
} |
||||
} |
||||
|
||||
function relayMatchesEntry(url: string, entry: string): boolean { |
||||
const normalized = normalizeAnyRelayUrl(url) || url.trim() |
||||
if (!normalized) return false |
||||
const entryNorm = normalizeAnyRelayUrl(entry) || entry.trim() |
||||
if (!entryNorm) return false |
||||
if (entryNorm === normalized) return true |
||||
const host = relayHostname(normalized) |
||||
return Boolean(host && relayHostname(entryNorm) === host) |
||||
} |
||||
|
||||
/** True when the relay matches an allowlist URL or shares its hostname (https vs wss). */ |
||||
export function isRelayInUserAllowlist(url: string, allowlist?: readonly string[]): boolean { |
||||
if (!allowlist?.length) return false |
||||
return allowlist.some((entry) => relayMatchesEntry(url, entry)) |
||||
} |
||||
|
||||
export function filterRelaysToUserAllowlist( |
||||
urls: readonly string[], |
||||
allowlist?: readonly string[] |
||||
): string[] { |
||||
if (!allowlist?.length) return [...urls] |
||||
const seen = new Set<string>() |
||||
const out: string[] = [] |
||||
for (const raw of urls) { |
||||
const n = normalizeAnyRelayUrl(raw) || raw.trim() |
||||
if (!n || !isRelayInUserAllowlist(n, allowlist)) continue |
||||
const k = n.toLowerCase() |
||||
if (seen.has(k)) continue |
||||
seen.add(k) |
||||
out.push(n) |
||||
} |
||||
return out |
||||
} |
||||
|
||||
/** |
||||
* When the session has recorded delivery relays, require at least one on the allowlist. |
||||
* Empty seen-on (e.g. fresh live REQ row) is treated as allowed. |
||||
*/ |
||||
export function eventSeenOnMatchesAllowlist( |
||||
seenRelayUrls: readonly string[], |
||||
allowlist: readonly string[] |
||||
): boolean { |
||||
if (!allowlist.length) return true |
||||
if (seenRelayUrls.length === 0) return true |
||||
return seenRelayUrls.some((u) => isRelayInUserAllowlist(u, allowlist)) |
||||
} |
||||
Loading…
Reference in new issue