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.
131 lines
4.5 KiB
131 lines
4.5 KiB
import { LRUCache } from 'lru-cache' |
|
import { buildViteProxySitesFetchUrl } from '@/lib/vite-proxy-url' |
|
import { isValidPubkey } from './pubkey' |
|
import { fetchWithTimeout } from '@/lib/fetch-with-timeout' |
|
import logger from '@/lib/logger' |
|
|
|
type TVerifyNip05Result = { |
|
isVerified: boolean |
|
nip05Name: string |
|
nip05Domain: string |
|
relays?: string[] |
|
} |
|
|
|
function asNip05LookupString(value: unknown): string { |
|
if (typeof value === 'string') return value |
|
if (value == null) return '' |
|
if (Array.isArray(value)) { |
|
for (const x of value) { |
|
if (typeof x === 'string' && x.trim()) return x |
|
} |
|
return '' |
|
} |
|
return String(value) |
|
} |
|
|
|
const verifyNip05ResultCache = new LRUCache<string, TVerifyNip05Result>({ |
|
max: 1000, |
|
fetchMethod: (key) => { |
|
const { nip05, pubkey } = JSON.parse(key) as { nip05?: unknown; pubkey?: unknown } |
|
return _verifyNip05(asNip05LookupString(nip05), typeof pubkey === 'string' ? pubkey : '') |
|
} |
|
}) |
|
|
|
async function _verifyNip05(nip05: string, pubkey: string): Promise<TVerifyNip05Result> { |
|
const nip05Str = asNip05LookupString(nip05) |
|
const parts = nip05Str ? nip05Str.split('@') : [] |
|
const nip05Name = parts[0] |
|
const nip05Domain = parts[1] |
|
const result: TVerifyNip05Result = { isVerified: false, nip05Name, nip05Domain } |
|
if (!nip05Name || !nip05Domain || !pubkey) return result |
|
|
|
const json = await fetchWellKnownNostrJson(nip05Domain, nip05Name) |
|
if (json) { |
|
const names = json.names as Record<string, string> | undefined |
|
if (names?.[nip05Name] === pubkey) { |
|
const relays = json.relays as Record<string, unknown> | undefined |
|
const relayList = relays?.[pubkey] |
|
return { ...result, isVerified: true, relays: Array.isArray(relayList) ? relayList : undefined } |
|
} |
|
} |
|
return result |
|
} |
|
|
|
export async function verifyNip05(nip05: string, pubkey: string): Promise<TVerifyNip05Result> { |
|
const nip05Str = asNip05LookupString(nip05) |
|
const pubkeyStr = typeof pubkey === 'string' ? pubkey : '' |
|
const result = await verifyNip05ResultCache.fetch(JSON.stringify({ nip05: nip05Str, pubkey: pubkeyStr })) |
|
if (result) { |
|
return result |
|
} |
|
const parts = nip05Str ? nip05Str.split('@') : [] |
|
return { isVerified: false, nip05Name: parts[0], nip05Domain: parts[1] } |
|
} |
|
|
|
export function getWellKnownNip05Url(domain: string, name?: string): string { |
|
const url = new URL('/.well-known/nostr.json', `https://${domain}`) |
|
if (name) { |
|
url.searchParams.set('name', name) |
|
} |
|
return url.toString() |
|
} |
|
|
|
/** |
|
* Fetch `/.well-known/nostr.json` in the browser without tripping third-party CORS: |
|
* when `VITE_PROXY_SERVER` is set (production), use same-origin `/sites/?url=…` like OG preview. |
|
*/ |
|
async function fetchWellKnownNostrJson(domain: string, name?: string): Promise<Record<string, unknown> | null> { |
|
const targetUrl = getWellKnownNip05Url(domain, name) |
|
const proxyServer = import.meta.env.VITE_PROXY_SERVER?.trim() |
|
const fetchUrl = proxyServer ? buildViteProxySitesFetchUrl(targetUrl, proxyServer) : targetUrl |
|
try { |
|
const res = await fetchWithTimeout(fetchUrl, { |
|
credentials: 'omit', |
|
headers: { Accept: 'application/json, text/plain;q=0.9,*/*;q=0.8' }, |
|
timeoutMs: 15_000 |
|
}) |
|
if (!res.ok) return null |
|
const data: unknown = await res.json() |
|
return data && typeof data === 'object' && !Array.isArray(data) ? (data as Record<string, unknown>) : null |
|
} catch { |
|
return null |
|
} |
|
} |
|
|
|
export async function fetchPubkeysFromDomain(domain: string): Promise<string[]> { |
|
try { |
|
const json = await fetchWellKnownNostrJson(domain) |
|
if (!json) return [] |
|
const pubkeySet = new Set<string>() |
|
return Object.values((json.names as Record<string, string>) || {}).filter((pubkey) => { |
|
if (typeof pubkey !== 'string' || !isValidPubkey(pubkey)) { |
|
return false |
|
} |
|
if (pubkeySet.has(pubkey)) { |
|
return false |
|
} |
|
pubkeySet.add(pubkey) |
|
return true |
|
}) as string[] |
|
} catch (error) { |
|
logger.error('Error fetching pubkeys from domain', { error, domain }) |
|
return [] |
|
} |
|
} |
|
|
|
/** |
|
* Attempt to get relays from NIP-07 extension |
|
* Some extensions support a getRelays() method |
|
*/ |
|
export async function getRelaysFromNip07Extension(): Promise<string[]> { |
|
try { |
|
if (window.nostr && typeof window.nostr.getRelays === 'function') { |
|
const relaysObj = await window.nostr.getRelays() |
|
// getRelays() returns an object like { "wss://relay.url": {read: true, write: true} } |
|
return Object.keys(relaysObj || {}) |
|
} |
|
} catch (error) { |
|
logger.warn('NIP-07 extension does not support getRelays()', error as Error) |
|
} |
|
return [] |
|
}
|
|
|