9 changed files with 247 additions and 43 deletions
@ -0,0 +1,41 @@ |
|||||||
|
import { describe, expect, it, beforeEach } from 'vitest' |
||||||
|
import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr' |
||||||
|
import { |
||||||
|
buildPersonalRelayKeySet, |
||||||
|
filterReadOnlyRelaysUnlessPersonal, |
||||||
|
isPersonalListRequiredReadOnlyRelay, |
||||||
|
setViewerPersonalRelayKeys |
||||||
|
} from './read-only-relay-personal' |
||||||
|
|
||||||
|
describe('read-only-relay-personal', () => { |
||||||
|
beforeEach(() => { |
||||||
|
setViewerPersonalRelayKeys(new Set()) |
||||||
|
}) |
||||||
|
|
||||||
|
it('requires personal list only for filter.nostr.wine', () => { |
||||||
|
expect(isPersonalListRequiredReadOnlyRelay('wss://filter.nostr.wine/')).toBe(true) |
||||||
|
expect(isPersonalListRequiredReadOnlyRelay('wss://relay.damus.io/')).toBe(false) |
||||||
|
expect(isPersonalListRequiredReadOnlyRelay(AGGR_NOSTR_LAND_WSS)).toBe(false) |
||||||
|
expect(isPersonalListRequiredReadOnlyRelay('wss://search.nos.today/')).toBe(false) |
||||||
|
}) |
||||||
|
|
||||||
|
it('strips unlisted filter.nostr.wine but keeps aggr and search indexers', () => { |
||||||
|
const urls = [ |
||||||
|
'wss://relay.damus.io/', |
||||||
|
'wss://filter.nostr.wine/', |
||||||
|
AGGR_NOSTR_LAND_WSS, |
||||||
|
'wss://search.nos.today/' |
||||||
|
] |
||||||
|
expect(filterReadOnlyRelaysUnlessPersonal(urls)).toEqual([ |
||||||
|
'wss://relay.damus.io/', |
||||||
|
AGGR_NOSTR_LAND_WSS, |
||||||
|
'wss://search.nos.today/' |
||||||
|
]) |
||||||
|
}) |
||||||
|
|
||||||
|
it('keeps filter.nostr.wine when on the viewer personal list', () => { |
||||||
|
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://filter.nostr.wine/'])) |
||||||
|
const urls = ['wss://relay.damus.io/', 'wss://filter.nostr.wine/'] |
||||||
|
expect(filterReadOnlyRelaysUnlessPersonal(urls)).toEqual(urls) |
||||||
|
}) |
||||||
|
}) |
||||||
@ -0,0 +1,67 @@ |
|||||||
|
import { READ_ONLY_PERSONAL_LIST_REQUIRED_RELAY_URLS } from '@/constants' |
||||||
|
import { normalizeAnyRelayUrl } from '@/lib/url' |
||||||
|
|
||||||
|
const personalListRequiredKeySet = new Set( |
||||||
|
READ_ONLY_PERSONAL_LIST_REQUIRED_RELAY_URLS.map((u) => |
||||||
|
(normalizeAnyRelayUrl(u) || u).toLowerCase() |
||||||
|
).filter(Boolean) |
||||||
|
) |
||||||
|
|
||||||
|
let viewerPersonalRelayKeys = new Set<string>() |
||||||
|
|
||||||
|
export function relayUrlKey(url: string): string { |
||||||
|
return (normalizeAnyRelayUrl(url) || url.trim()).toLowerCase() |
||||||
|
} |
||||||
|
|
||||||
|
/** True when the relay must be on the viewer's personal lists before connect / AUTH. */ |
||||||
|
export function isPersonalListRequiredReadOnlyRelay(url: string): boolean { |
||||||
|
const key = relayUrlKey(url) |
||||||
|
return key.length > 0 && personalListRequiredKeySet.has(key) |
||||||
|
} |
||||||
|
|
||||||
|
/** @deprecated Use {@link isPersonalListRequiredReadOnlyRelay}; kept for NIP-42 call sites. */ |
||||||
|
export function isReadOnlyIndexerRelay(url: string): boolean { |
||||||
|
return isPersonalListRequiredReadOnlyRelay(url) |
||||||
|
} |
||||||
|
|
||||||
|
export function buildPersonalRelayKeySet(urls: readonly string[]): Set<string> { |
||||||
|
const out = new Set<string>() |
||||||
|
for (const u of urls) { |
||||||
|
const key = relayUrlKey(u) |
||||||
|
if (key) out.add(key) |
||||||
|
} |
||||||
|
return out |
||||||
|
} |
||||||
|
|
||||||
|
/** Updated when the logged-in viewer's NIP-65 / favorites / cache relays hydrate. */ |
||||||
|
export function setViewerPersonalRelayKeys(keys: ReadonlySet<string>): void { |
||||||
|
viewerPersonalRelayKeys = new Set(keys) |
||||||
|
} |
||||||
|
|
||||||
|
export function getViewerPersonalRelayKeys(): ReadonlySet<string> { |
||||||
|
return viewerPersonalRelayKeys |
||||||
|
} |
||||||
|
|
||||||
|
export function isReadOnlyRelayAllowedForViewer(url: string): boolean { |
||||||
|
if (!isPersonalListRequiredReadOnlyRelay(url)) return true |
||||||
|
const key = relayUrlKey(url) |
||||||
|
return key.length > 0 && viewerPersonalRelayKeys.has(key) |
||||||
|
} |
||||||
|
|
||||||
|
function isAllowedForKeys(url: string, personalKeys: ReadonlySet<string>): boolean { |
||||||
|
if (!isPersonalListRequiredReadOnlyRelay(url)) return true |
||||||
|
const key = relayUrlKey(url) |
||||||
|
return key.length > 0 && personalKeys.has(key) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Drop {@link READ_ONLY_PERSONAL_LIST_REQUIRED_RELAY_URLS} unless the viewer listed them on NIP-65 / favorites / 10432. |
||||||
|
* Other read-only index relays (aggr.nostr.land, search.nos.today, …) are unchanged. |
||||||
|
*/ |
||||||
|
export function filterReadOnlyRelaysUnlessPersonal( |
||||||
|
urls: readonly string[], |
||||||
|
personalKeys?: ReadonlySet<string> |
||||||
|
): string[] { |
||||||
|
const keys = personalKeys ?? viewerPersonalRelayKeys |
||||||
|
return urls.filter((u) => isAllowedForKeys(u, keys)) |
||||||
|
} |
||||||
Loading…
Reference in new issue