First slice of TRUST_SCORE.md. - lib/nip85.ts: parse kind-10040 provider designations, resolve a provider with a house fallback, parse kind-30382 assertions (rank/followers/ reporters/muters/hops; absent hops means no path, absence is not distrust), and author-check the provider. - hooks/useTrustAssertions.ts: batch read from the resolved provider, signature-verified via onlyVerified + the author check. - lib/trustLadder.ts: three buckets and thresholds (flagged = trusted reporters >= 2; verified rank >= 2). - hooks/useTrustBucket.ts: prefers a NIP-85 assertion (verify + flag), falls back to the local WoT ladder (verify only). - TrustBadge.tsx: now renders Verified / Unknown / Flagged with glyph, colour and tooltip; Unknown hidden by default, Flagged always shown. Reaches the map/tour/event cards, marketplace card, listing detail and coach detail through the shared component. Open follow-ups (in TRUST_SCORE.md): batch the bucket at list level, a 'who reported' breakdown, a provider-choice setting, alerts, confidence scoring.
93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
|
|
import { useNostr } from '@nostrify/react';
|
|
import type { NostrEvent } from '@nostrify/nostrify';
|
|
import { useCurrentUser } from './useCurrentUser';
|
|
import { onlyVerified } from '@/lib/verifyEvents';
|
|
import {
|
|
HOUSE_TRUST_PROVIDER,
|
|
NIP85_PROVIDER_LIST_KIND,
|
|
NIP85_USER_ASSERTION_KIND,
|
|
parseProviderDesignation,
|
|
parseUserTrustAssertions,
|
|
resolveProvider,
|
|
type TrustProvider,
|
|
type UserTrustAssertion,
|
|
} from '@/lib/nip85';
|
|
|
|
const MAX_TARGETS = 500;
|
|
|
|
/**
|
|
* Read kind-30382 Trusted Assertions (NIP-85) for a batch of subjects, from the
|
|
* provider the viewer designates via their own kind-10040 — falling back to the
|
|
* configured house provider. Signatures are verified and the event author must
|
|
* equal the resolved provider before a score is used.
|
|
*
|
|
* A missing assertion yields no entry: that means "nothing published", which is
|
|
* NOT the same as distrust. Callers must treat absence as `unknown`.
|
|
*/
|
|
export function useTrustAssertions(pubkeys: string[]) {
|
|
const { nostr } = useNostr();
|
|
const { user } = useCurrentUser();
|
|
|
|
const targets = [...pubkeys].sort().slice(0, MAX_TARGETS);
|
|
|
|
return useQuery({
|
|
queryKey: ['trust-assertions', targets.join(','), user?.pubkey],
|
|
queryFn: async (c): Promise<Map<string, UserTrustAssertion>> => {
|
|
const out = new Map<string, UserTrustAssertion>();
|
|
if (targets.length === 0) return out;
|
|
|
|
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(8000)]);
|
|
|
|
// 1. Resolve the provider: the viewer's own designation, else the house.
|
|
let provider: TrustProvider = HOUSE_TRUST_PROVIDER;
|
|
try {
|
|
if (user?.pubkey) {
|
|
const designationEvents = onlyVerified(await nostr.query(
|
|
[{ kinds: [NIP85_PROVIDER_LIST_KIND], authors: [user.pubkey], limit: 1 }],
|
|
{ signal },
|
|
));
|
|
const designation = parseProviderDesignation(designationEvents[0]);
|
|
provider = resolveProvider(
|
|
designation,
|
|
NIP85_USER_ASSERTION_KIND,
|
|
'rank',
|
|
HOUSE_TRUST_PROVIDER,
|
|
);
|
|
}
|
|
} catch {
|
|
// Designation lookup failed — the house provider is the fallback.
|
|
}
|
|
|
|
// 2. Fetch the assertions for these subjects from the resolved provider.
|
|
const filter = {
|
|
kinds: [NIP85_USER_ASSERTION_KIND],
|
|
authors: [provider.pubkey],
|
|
'#d': targets,
|
|
limit: targets.length,
|
|
};
|
|
|
|
try {
|
|
let events: NostrEvent[];
|
|
if (provider.relay) {
|
|
try {
|
|
events = await nostr.relay(provider.relay).query([filter], { signal });
|
|
} catch {
|
|
events = await nostr.query([filter], { signal });
|
|
}
|
|
} else {
|
|
events = await nostr.query([filter], { signal });
|
|
}
|
|
return parseUserTrustAssertions(onlyVerified(events), provider.pubkey);
|
|
} catch {
|
|
// Provider unreachable — absence, not distrust.
|
|
return out;
|
|
}
|
|
},
|
|
enabled: targets.length > 0,
|
|
staleTime: 60 * 60 * 1000, // 1h
|
|
gcTime: 24 * 60 * 60 * 1000,
|
|
retry: 1,
|
|
});
|
|
}
|