// Shared NIP-05 verification helpers. // // NIP-05 is a *soft* trust signal, not a gate: it proves the pubkey controls a // `local@domain` handle whose `.well-known/nostr.json` maps that name back to // the same pubkey. SkateSpots never gatekeeps the handle or the domain — any // domain the user controls works, keeping verification fully decentralized. export type Nip05Status = 'none' | 'verified' | 'mismatch' | 'unreachable'; export interface Nip05Result { status: Nip05Status; /** The pubkey the domain claims for this name, if any. */ resolvedPubkey?: string; } /** Basic `local@domain.tld` shape check. */ export function parseNip05(nip05: string): { name: string; domain: string } | null { const trimmed = nip05.trim().toLowerCase(); const at = trimmed.indexOf('@'); if (at <= 0) return null; const name = trimmed.slice(0, at); const domain = trimmed.slice(at + 1); if (!/^[a-z0-9._-]+$/.test(name)) return null; if (!/^[a-z0-9.-]+\.[a-z]{2,}$/.test(domain)) return null; return { name, domain }; } /** * Fetch a domain's `.well-known/nostr.json` name map. Tries the domain directly * first, then falls back to the CORS proxy (many nostr.json hosts don't send * permissive CORS headers). */ export async function fetchWellKnown( name: string, domain: string, signal: AbortSignal, ): Promise | null> { const target = `https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`; const attempts = [ target, `https://proxy.shakespeare.diy/?url=${encodeURIComponent(target)}`, ]; for (const url of attempts) { try { const res = await fetch(url, { signal, headers: { Accept: 'application/json' } }); if (!res.ok) continue; const data = await res.json(); const names = data?.names; if (names && typeof names === 'object') { return names as Record; } } catch { // try next attempt } } return null; } /** * Verify a NIP-05 identifier against the pubkey that is supposed to own it. * * - `verified`: domain maps the name to `expectedPubkey`. Trusted ✓ * - `mismatch`: domain maps the name to a DIFFERENT pubkey. Do not trust. * - `unreachable`: the domain / nostr.json couldn't be fetched (network/CORS). * - `none`: no NIP-05 set, or malformed. */ export async function verifyNip05( nip05: string | undefined, expectedPubkey: string | undefined, signal: AbortSignal, ): Promise { if (!nip05 || !expectedPubkey) return { status: 'none' }; const parsed = parseNip05(nip05); if (!parsed) return { status: 'none' }; const names = await fetchWellKnown(parsed.name, parsed.domain, signal); if (!names) return { status: 'unreachable' }; const resolved = names[parsed.name]?.toLowerCase(); if (!resolved) return { status: 'unreachable' }; if (resolved === expectedPubkey.toLowerCase()) { return { status: 'verified', resolvedPubkey: resolved }; } return { status: 'mismatch', resolvedPubkey: resolved }; }