SkateSpots/src/lib/featuredVerification.ts
SkateSpots a745bdbbcf fix(trust): C5b — featured ranking requires a verified provider receipt
A ['featured', expiry] tag is self-asserted, so anyone could mint one and buy
top placement (the badge verified it, ordering didn't).

- New src/lib/featuredVerification.ts resolves the provider pubkey (cached) and
  batch-checks provider-authored kind-9735 zap receipts for the featured coords.
- useTours / useSkateEvents / useUpcomingSkateEvents / useLabProfiles now rank
  only receipts-backed featured items first.
- Availability fallback: if the provider can't be resolved or the query fails,
  the self-asserted ordering is kept, so a provider outage never blanks paid
  placement.
2026-10-02 23:28:34 +07:00

54 lines
1.9 KiB
TypeScript

/**
* Featured ("Pro") ranking verification.
*
* SECURITY: a `["featured", <expiry>]` tag is SELF-ASSERTED — anyone can add it
* and get top placement, and `useFeaturedVerification` only downgrades the badge,
* never the ordering. This helper resolves which featured coordinates are backed
* by a **provider-authored NIP-57 zap receipt** so list hooks can rank only those.
*
* Availability over strictness: if the provider pubkey can't be resolved or the
* receipt query fails, it returns `null` and the caller keeps the self-asserted
* ordering (so a provider outage never blanks paid placement).
*/
import { PRO_LIGHTNING_ADDRESS } from '@/lib/sasquatchPro';
import { getZapProviderPubkey } from '@/lib/lightningAddress';
import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions';
import type { NostrEvent, NostrFilter } from '@nostrify/nostrify';
let providerPromise: Promise<string | null> | null = null;
function cachedProvider(): Promise<string | null> {
if (!providerPromise) {
providerPromise = getZapProviderPubkey(PRO_LIGHTNING_ADDRESS).catch(() => null);
}
return providerPromise;
}
/**
* @returns the set of featured coords with a verified provider receipt, or
* `null` when verification is unavailable (caller should not demote).
*/
export async function verifiedFeaturedCoords(
coords: string[],
query: (filter: NostrFilter) => Promise<NostrEvent[]>,
): Promise<Set<string> | null> {
if (coords.length === 0) return null;
const provider = await cachedProvider();
if (!provider) return null;
try {
const receipts = await query({
kinds: [9735],
authors: [provider],
'#a': coords,
'#p': PLATFORM_OWNER_PUBKEYS,
limit: coords.length * 4,
});
const set = new Set<string>();
for (const r of receipts) {
for (const [n, v] of r.tags) if (n === 'a' && v) set.add(v);
}
return set;
} catch {
return null;
}
}