You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
160 lines
5.7 KiB
160 lines
5.7 KiB
import { |
|
READ_ONLY_PERSONAL_LIST_REQUIRED_RELAY_URLS |
|
} from '@/constants' |
|
import { isMetadataPolicyProfileRelay } from '@/lib/metadata-policy-curated-relays' |
|
import { |
|
filterAggrNostrLandUnlessViewerEligible, |
|
getViewerRelayStackNostrLandAggrEligible, |
|
relayUrlIsAggrNostrLand |
|
} from '@/lib/nostr-land-relay-eligibility' |
|
import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize' |
|
import { filterViewerBlockedRelaysForFetch } from '@/lib/viewer-blocked-relays' |
|
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>() |
|
/** True after a logged-in viewer's personal relay keys were synced (including empty lists). */ |
|
let viewerMetadataRelaysPolicyActive = false |
|
let restrictConnectionsToMetadataRelaysOnly = false |
|
/** Relay explore / search UI: metadata-only policy must not narrow relays on those pages. */ |
|
let metadataRelaysOnlyBypassDepth = 0 |
|
|
|
export function setRestrictConnectionsToMetadataRelaysOnly(enabled: boolean): void { |
|
restrictConnectionsToMetadataRelaysOnly = enabled |
|
} |
|
|
|
export function isRestrictConnectionsToMetadataRelaysOnly(): boolean { |
|
return restrictConnectionsToMetadataRelaysOnly |
|
} |
|
|
|
export function enterMetadataRelaysOnlyBypass(): void { |
|
metadataRelaysOnlyBypassDepth++ |
|
} |
|
|
|
export function leaveMetadataRelaysOnlyBypass(): void { |
|
metadataRelaysOnlyBypassDepth = Math.max(0, metadataRelaysOnlyBypassDepth - 1) |
|
} |
|
|
|
export function isMetadataRelaysOnlyBypassActive(): boolean { |
|
return metadataRelaysOnlyBypassDepth > 0 |
|
} |
|
|
|
/** Logged-in viewer with metadata-only mode: only connect reads to the viewer's relay lists. */ |
|
export function isMetadataRelaysOnlyPolicyActive(): boolean { |
|
return ( |
|
restrictConnectionsToMetadataRelaysOnly && |
|
viewerMetadataRelaysPolicyActive && |
|
!isMetadataRelaysOnlyBypassActive() |
|
) |
|
} |
|
|
|
export function isRelayUrlInViewerMetadataLists(url: string): boolean { |
|
const key = relayUrlKey(url) |
|
return key.length > 0 && viewerPersonalRelayKeys.has(key) |
|
} |
|
|
|
/** |
|
* Under metadata-only policy: viewer NIP-65 / favorites / cache / HTTP lists, plus aggr.nostr.land when |
|
* wss://nostr.land is listed, plus {@link PROFILE_RELAY_URLS} for kind-0 / profile hydration. |
|
*/ |
|
export function isRelayAllowedUnderMetadataOnlyPolicy(url: string): boolean { |
|
if (isRelayUrlInViewerMetadataLists(url)) return true |
|
if (getViewerRelayStackNostrLandAggrEligible() && relayUrlIsAggrNostrLand(url)) return true |
|
if (isMetadataPolicyProfileRelay(url)) return true |
|
return false |
|
} |
|
|
|
/** Block read-side pool connects / HTTP index fetches when metadata-only policy is on. */ |
|
export function isRelayConnectionAllowedForViewer(url: string): boolean { |
|
if (!isMetadataRelaysOnlyPolicyActive()) return true |
|
return isRelayAllowedUnderMetadataOnlyPolicy(url) |
|
} |
|
|
|
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>, |
|
policy?: { viewerActive?: boolean } |
|
): void { |
|
viewerPersonalRelayKeys = new Set(keys) |
|
if (policy?.viewerActive !== undefined) { |
|
viewerMetadataRelaysPolicyActive = policy.viewerActive |
|
} |
|
} |
|
|
|
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)) |
|
} |
|
|
|
/** |
|
* Sanitize relay URLs assembled for REQ/fetch: drop other people's LAN/localhost hints (keep viewer's |
|
* own locals from NIP-65 / favorites / 10432), then gated read-only indexers. |
|
*/ |
|
export function sanitizeRelayUrlsForFetch( |
|
urls: readonly string[], |
|
personalKeys?: ReadonlySet<string> |
|
): string[] { |
|
const keys = personalKeys ?? viewerPersonalRelayKeys |
|
const withoutThirdPartyLocals = urls.filter((u) => { |
|
if (urlIsNonLocalForRemoteViewer(u)) return true |
|
const key = relayUrlKey(u) |
|
return key.length > 0 && keys.has(key) |
|
}) |
|
return filterViewerBlockedRelaysForFetch( |
|
filterAggrNostrLandUnlessViewerEligible( |
|
filterReadOnlyRelaysUnlessPersonal(withoutThirdPartyLocals, keys) |
|
) |
|
) |
|
}
|
|
|