|
|
|
@ -450,6 +450,9 @@ class ClientService extends EventTarget { |
|
|
|
if (params?.purpose !== 'write' && !isRelayConnectionAllowedForViewer(url)) { |
|
|
|
if (params?.purpose !== 'write' && !isRelayConnectionAllowedForViewer(url)) { |
|
|
|
throw new Error(`[metadata-relays-only] skipping relay ${url}`) |
|
|
|
throw new Error(`[metadata-relays-only] skipping relay ${url}`) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (params?.purpose !== 'write' && relaySessionStrikes.isReadHttpSkipped(url)) { |
|
|
|
|
|
|
|
throw new Error(`[relay-strike] skipping unresponsive relay ${url}`) |
|
|
|
|
|
|
|
} |
|
|
|
if (!isWebsocketUrl(url) && isKind10243HttpRelayTagUrl(url)) { |
|
|
|
if (!isWebsocketUrl(url) && isKind10243HttpRelayTagUrl(url)) { |
|
|
|
throw new Error(`[http-index-relay] ${url} uses the HTTPS index API, not WebSocket`) |
|
|
|
throw new Error(`[http-index-relay] ${url} uses the HTTPS index API, not WebSocket`) |
|
|
|
} |
|
|
|
} |
|
|
|
@ -458,10 +461,25 @@ class ClientService extends EventTarget { |
|
|
|
const connectionTimeout = READ_ONLY_RELAY_CONNECT_BOOST_URLS.has(n) |
|
|
|
const connectionTimeout = READ_ONLY_RELAY_CONNECT_BOOST_URLS.has(n) |
|
|
|
? Math.max(base, RELAY_READ_ONLY_POOL_CONNECT_TIMEOUT_MS) |
|
|
|
? Math.max(base, RELAY_READ_ONLY_POOL_CONNECT_TIMEOUT_MS) |
|
|
|
: base |
|
|
|
: base |
|
|
|
const relay = await rawEnsureRelay(url, { |
|
|
|
let relay |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
relay = await rawEnsureRelay(url, { |
|
|
|
...params, |
|
|
|
...params, |
|
|
|
connectionTimeout |
|
|
|
connectionTimeout |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
|
|
const msg = err instanceof Error ? err.message : String(err) |
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
|
|
params?.purpose !== 'write' && |
|
|
|
|
|
|
|
!msg.includes('[metadata-relays-only]') && |
|
|
|
|
|
|
|
!msg.includes('[relay-strike]') && |
|
|
|
|
|
|
|
!msg.includes('[offline]') && |
|
|
|
|
|
|
|
!msg.includes('[http-index-relay]') |
|
|
|
|
|
|
|
) { |
|
|
|
|
|
|
|
relaySessionStrikes.recordReadFailure(url, 'connection') |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
throw err |
|
|
|
|
|
|
|
} |
|
|
|
patchPoolRelayAuthRaceAndFeedback(relay) |
|
|
|
patchPoolRelayAuthRaceAndFeedback(relay) |
|
|
|
applyRelayNip42AckTimeout(relay) |
|
|
|
applyRelayNip42AckTimeout(relay) |
|
|
|
touchRelayPoolActivity(url) |
|
|
|
touchRelayPoolActivity(url) |
|
|
|
@ -631,22 +649,50 @@ class ClientService extends EventTarget { |
|
|
|
setViewerPersonalRelayKeys(new Set(), { viewerActive: false }) |
|
|
|
setViewerPersonalRelayKeys(new Set(), { viewerActive: false }) |
|
|
|
syncViewerRelayStackNostrLandAggrEligible([]) |
|
|
|
syncViewerRelayStackNostrLandAggrEligible([]) |
|
|
|
setViewerBlockedRelayUrls([]) |
|
|
|
setViewerBlockedRelayUrls([]) |
|
|
|
|
|
|
|
relaySessionStrikes.setSessionCacheRelayKeysFromKind10432(null) |
|
|
|
return |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
/** Engage policy before any await so session hydrate cannot open PROFILE/FAST_WRITE stacks first. */ |
|
|
|
|
|
|
|
if (isRestrictConnectionsToMetadataRelaysOnly()) { |
|
|
|
/** IndexedDB-first: personal lists (incl. cache + HTTP) before policy or network so locals stay allowed. */ |
|
|
|
setViewerPersonalRelayKeys(new Set(), { viewerActive: true }) |
|
|
|
const storageUrls = await this.collectViewerPersonalRelayUrlsFromStorage(pk) |
|
|
|
} |
|
|
|
this.viewerHttpIndexRelayBases = storageUrls.httpIndexBases |
|
|
|
|
|
|
|
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(storageUrls.all), { |
|
|
|
|
|
|
|
viewerActive: isRestrictConnectionsToMetadataRelaysOnly() |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
syncViewerRelayStackNostrLandAggrEligible(storageUrls.all) |
|
|
|
|
|
|
|
relaySessionStrikes.setSessionCacheRelayKeysFromKind10432(storageUrls.cacheRelayEvent) |
|
|
|
|
|
|
|
this.closeMetadataPolicyDisallowedRelayConnections() |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
try { |
|
|
|
const blockedEvt = await indexedDb.getReplaceableEvent(pk, ExtendedKind.BLOCKED_RELAYS) |
|
|
|
const blockedEvt = await indexedDb.getReplaceableEvent(pk, ExtendedKind.BLOCKED_RELAYS) |
|
|
|
setViewerBlockedRelayUrls(parseBlockedRelayUrlsFromEvent(blockedEvt ?? null)) |
|
|
|
setViewerBlockedRelayUrls(parseBlockedRelayUrlsFromEvent(blockedEvt ?? null)) |
|
|
|
} catch { |
|
|
|
} catch { |
|
|
|
setViewerBlockedRelayUrls([]) |
|
|
|
setViewerBlockedRelayUrls([]) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const urls = [...storageUrls.all] |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
urls.push(...(await this.fetchFavoriteRelays(pk))) |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(urls), { viewerActive: true }) |
|
|
|
|
|
|
|
syncViewerRelayStackNostrLandAggrEligible(urls) |
|
|
|
|
|
|
|
this.closeMetadataPolicyDisallowedRelayConnections() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** NIP-65 / 10243 / 10432 / favorites (10012) from IndexedDB only — no network. */ |
|
|
|
|
|
|
|
private async collectViewerPersonalRelayUrlsFromStorage(pubkey: string): Promise<{ |
|
|
|
|
|
|
|
all: string[] |
|
|
|
|
|
|
|
httpIndexBases: string[] |
|
|
|
|
|
|
|
cacheRelayEvent: NEvent | undefined |
|
|
|
|
|
|
|
}> { |
|
|
|
const urls: string[] = [] |
|
|
|
const urls: string[] = [] |
|
|
|
|
|
|
|
let httpIndexBases: string[] = [] |
|
|
|
|
|
|
|
let cacheRelayEvent: NEvent | undefined |
|
|
|
try { |
|
|
|
try { |
|
|
|
const rl = await this.peekRelayListFromStorage(pk) |
|
|
|
const rl = await this.peekRelayListFromStorage(pubkey) |
|
|
|
this.viewerHttpIndexRelayBases = [...(rl.httpRead ?? []), ...(rl.httpWrite ?? [])] |
|
|
|
httpIndexBases = [...(rl.httpRead ?? []), ...(rl.httpWrite ?? [])] |
|
|
|
.map((u) => normalizeHttpRelayUrl(u) || u) |
|
|
|
.map((u) => normalizeHttpRelayUrl(u) || u) |
|
|
|
.filter(Boolean) |
|
|
|
.filter(Boolean) |
|
|
|
urls.push( |
|
|
|
urls.push( |
|
|
|
@ -656,22 +702,32 @@ class ClientService extends EventTarget { |
|
|
|
...(rl.httpWrite ?? []) |
|
|
|
...(rl.httpWrite ?? []) |
|
|
|
) |
|
|
|
) |
|
|
|
} catch { |
|
|
|
} catch { |
|
|
|
this.viewerHttpIndexRelayBases = [] |
|
|
|
httpIndexBases = [] |
|
|
|
// ignore
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
try { |
|
|
|
try { |
|
|
|
urls.push(...(await this.fetchFavoriteRelays(pk))) |
|
|
|
urls.push(...(await this.fetchFavoriteRelaysFromStorage(pubkey))) |
|
|
|
} catch { |
|
|
|
} catch { |
|
|
|
// ignore
|
|
|
|
// ignore
|
|
|
|
} |
|
|
|
} |
|
|
|
try { |
|
|
|
try { |
|
|
|
urls.push(...(await getCacheRelayUrls(pk))) |
|
|
|
cacheRelayEvent = (await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS)) ?? undefined |
|
|
|
|
|
|
|
urls.push(...(await getCacheRelayUrls(pubkey))) |
|
|
|
} catch { |
|
|
|
} catch { |
|
|
|
// ignore
|
|
|
|
cacheRelayEvent = undefined |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
const all = Array.from(new Set(urls.map((u) => u.trim()).filter(Boolean))) |
|
|
|
|
|
|
|
return { all, httpIndexBases, cacheRelayEvent } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Kind 10012 + embedded NIP-51 relay sets from IndexedDB only. */ |
|
|
|
|
|
|
|
private async fetchFavoriteRelaysFromStorage(pubkey: string): Promise<string[]> { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
const favoriteRelaysEvent = await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.FAVORITE_RELAYS) |
|
|
|
|
|
|
|
if (!favoriteRelaysEvent) return [] |
|
|
|
|
|
|
|
return await this.expandFavoriteRelayUrlsFromEvent(pubkey, favoriteRelaysEvent) |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
return [] |
|
|
|
} |
|
|
|
} |
|
|
|
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(urls), { viewerActive: true }) |
|
|
|
|
|
|
|
syncViewerRelayStackNostrLandAggrEligible(urls) |
|
|
|
|
|
|
|
this.closeMetadataPolicyDisallowedRelayConnections() |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** Drop pooled WebSocket connections that violate the metadata-only read policy. */ |
|
|
|
/** Drop pooled WebSocket connections that violate the metadata-only read policy. */ |
|
|
|
@ -3565,9 +3621,21 @@ class ClientService extends EventTarget { |
|
|
|
|
|
|
|
|
|
|
|
async fetchFavoriteRelays(pubkey: string): Promise<string[]> { |
|
|
|
async fetchFavoriteRelays(pubkey: string): Promise<string[]> { |
|
|
|
try { |
|
|
|
try { |
|
|
|
const favoriteRelaysEvent = await this.replaceableEventService.fetchReplaceableEvent(pubkey, ExtendedKind.FAVORITE_RELAYS) |
|
|
|
const favoriteRelaysEvent = await this.replaceableEventService.fetchReplaceableEvent( |
|
|
|
|
|
|
|
pubkey, |
|
|
|
|
|
|
|
ExtendedKind.FAVORITE_RELAYS |
|
|
|
|
|
|
|
) |
|
|
|
if (!favoriteRelaysEvent) return [] |
|
|
|
if (!favoriteRelaysEvent) return [] |
|
|
|
|
|
|
|
return await this.expandFavoriteRelayUrlsFromEvent(pubkey, favoriteRelaysEvent) |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
return [] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async expandFavoriteRelayUrlsFromEvent( |
|
|
|
|
|
|
|
pubkey: string, |
|
|
|
|
|
|
|
favoriteRelaysEvent: NEvent |
|
|
|
|
|
|
|
): Promise<string[]> { |
|
|
|
const relays: string[] = [] |
|
|
|
const relays: string[] = [] |
|
|
|
const relaySetIds: string[] = [] |
|
|
|
const relaySetIds: string[] = [] |
|
|
|
favoriteRelaysEvent.tags.forEach(([tagName, tagValue]) => { |
|
|
|
favoriteRelaysEvent.tags.forEach(([tagName, tagValue]) => { |
|
|
|
@ -3589,7 +3657,6 @@ class ClientService extends EventTarget { |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
// NIP-51 relay sets on kind 10012: same expansion as {@link FavoriteRelaysProvider} (not only `relay` tags).
|
|
|
|
|
|
|
|
for (const id of relaySetIds) { |
|
|
|
for (const id of relaySetIds) { |
|
|
|
try { |
|
|
|
try { |
|
|
|
const ev = await indexedDb.getReplaceableEvent(pubkey, kinds.Relaysets, id) |
|
|
|
const ev = await indexedDb.getReplaceableEvent(pubkey, kinds.Relaysets, id) |
|
|
|
@ -3605,9 +3672,6 @@ class ClientService extends EventTarget { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return Array.from(new Set(relays)) |
|
|
|
return Array.from(new Set(relays)) |
|
|
|
} catch { |
|
|
|
|
|
|
|
return [] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|