Keeps the skater directory fully open — NIP-05 is a trust signal, not a gate: - Extract reusable NIP-05 verification into lib/nip05.ts (parse, fetch well-known, verify against expected pubkey); refactor useCrewNip05 to use it - New useSkaterVerifications hook: batch-fetches kind-0 metadata for all skaters and verifies each nip05 resolves to its own pubkey, returning a Set of verified pubkeys - SkatersPage: add a "Verified (N)" toggle (disabled when none verified) that filters to genuinely verified skaters; show ✓ badge only for truly-verified skaters; verified-aware empty state - NIP19Page profile: nip05 line now reflects real verification (lime ShieldCheck + "Verified" pill when the handle resolves to the key, muted grey otherwise) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
69 lines
2.6 KiB
TypeScript
69 lines
2.6 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { useNostr } from '@nostrify/react';
|
|
import { verifyNip05 } from '@/lib/nip05';
|
|
|
|
/**
|
|
* Batch-verify NIP-05 for a set of skater pubkeys.
|
|
*
|
|
* NIP-05 is a *soft* trust signal, never a gate — the whole directory stays open
|
|
* to everyone. This hook simply fetches each skater's kind-0 metadata, then
|
|
* confirms their `nip05` handle actually resolves to their own pubkey via the
|
|
* domain's `.well-known/nostr.json`. The result powers the "Verified only"
|
|
* filter and the verified badge, letting people *opt in* to a higher-trust view
|
|
* without excluding anyone.
|
|
*
|
|
* Returns a Set of pubkeys that are genuinely NIP-05 verified.
|
|
*/
|
|
export function useSkaterVerifications(pubkeys: string[]) {
|
|
const { nostr } = useNostr();
|
|
|
|
// Stable key so the query only refires when the actual set changes.
|
|
const sortedKey = [...pubkeys].sort().join(',');
|
|
|
|
return useQuery({
|
|
queryKey: ['skater-verifications', sortedKey],
|
|
enabled: pubkeys.length > 0,
|
|
staleTime: 10 * 60 * 1000, // 10 min — verification rarely changes
|
|
gcTime: 30 * 60 * 1000,
|
|
queryFn: async (c): Promise<Set<string>> => {
|
|
const verified = new Set<string>();
|
|
if (pubkeys.length === 0) return verified;
|
|
|
|
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(12000)]);
|
|
|
|
// 1. Fetch kind-0 metadata for all skaters in one query.
|
|
const profiles = await nostr.query([{ kinds: [0], authors: pubkeys }], { signal });
|
|
|
|
// Map pubkey -> nip05 handle (newest metadata wins).
|
|
const nip05ByPubkey = new Map<string, string>();
|
|
const seenAt = new Map<string, number>();
|
|
for (const ev of profiles) {
|
|
const prev = seenAt.get(ev.pubkey) ?? -1;
|
|
if (ev.created_at <= prev) continue;
|
|
seenAt.set(ev.pubkey, ev.created_at);
|
|
try {
|
|
const meta = JSON.parse(ev.content) as { nip05?: string };
|
|
if (meta.nip05) nip05ByPubkey.set(ev.pubkey, meta.nip05);
|
|
else nip05ByPubkey.delete(ev.pubkey);
|
|
} catch {
|
|
// ignore malformed metadata
|
|
}
|
|
}
|
|
|
|
// 2. Verify each handle against its own pubkey, in parallel.
|
|
const entries = [...nip05ByPubkey.entries()];
|
|
const results = await Promise.all(
|
|
entries.map(async ([pubkey, nip05]) => {
|
|
const res = await verifyNip05(nip05, pubkey, signal).catch(() => ({ status: 'unreachable' as const }));
|
|
return { pubkey, ok: res.status === 'verified' };
|
|
}),
|
|
);
|
|
|
|
for (const { pubkey, ok } of results) {
|
|
if (ok) verified.add(pubkey);
|
|
}
|
|
|
|
return verified;
|
|
},
|
|
});
|
|
}
|