12 changed files with 224 additions and 20 deletions
@ -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 @@ |
|||||||
|
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