Fixes the audit findings: HIGH-1 (NWC/WebLN auto-pay of unverified invoices): add assertInvoiceAmount and call it before every programmatic payment in useZaps, useCrewZap, and SpamFeePayment. A malicious LNURL/Lightning-address server can no longer return an inflated invoice and drain the wallet — amount is decoded from bolt11 and must match the intended sats (±1 sat), amountless invoices are rejected. HIGH-2 (forgeable trust signals): - useSasquatchPro: Pro is granted ONLY by a zap receipt AUTHORED by the platform's Lightning provider, embedding a user-signed zap request that tags the platform, with a real paid amount >= smallest tier. The self-signed kind:32671 receipt is now advisory only and never grants access (closed the "self-grant permanent Pro" hole). - useFeaturedVerification: a listing is 'verified' only when the matching 9735 is authored by the provider pubkey (not any self-published receipt). - useCrewGoal: tally only invoice-backed (bolt11) zaps; the forgeable amount tag is no longer summed, so goal progress can't be inflated with fake receipts. Bump to 1.2.14. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { useNostr } from '@nostrify/react';
|
|
import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions';
|
|
import { getZapProviderPubkey } from '@/lib/lightningAddress';
|
|
import { PRO_LIGHTNING_ADDRESS } from '@/lib/sasquatchPro';
|
|
import { isFeaturedActive } from '@/lib/types';
|
|
|
|
export type FeaturedStatus = 'none' | 'claimed' | 'verified';
|
|
|
|
/**
|
|
* Verify a "featured"/Pro listing against NIP-57 zap receipts.
|
|
*
|
|
* SECURITY: a receipt only counts as `verified` when it is AUTHORED by the
|
|
* platform's Lightning provider pubkey. Previously any kind-9735 with matching
|
|
* `#a`/`#p` tags was accepted — but anyone can publish such an event themselves,
|
|
* so a scammer could self-mint a "Verified payment" badge. Requiring the
|
|
* provider as author makes the receipt unforgeable.
|
|
*
|
|
* Layered / best-effort:
|
|
* - `verified`: a provider-authored kind-9735 zap receipt paying the platform
|
|
* and referencing this entity's `a` coordinate was found. Tamper-proof.
|
|
* - `claimed`: the event carries an unexpired `featured` tag but no verifiable
|
|
* provider receipt could be found. Self-attested — shown, but NOT verified.
|
|
* - `none`: not featured (or the featured window has expired).
|
|
*/
|
|
export function useFeaturedVerification(
|
|
entityCoord: string | undefined,
|
|
featuredUntil: number | undefined,
|
|
) {
|
|
const { nostr } = useNostr();
|
|
|
|
const claimed = isFeaturedActive(featuredUntil);
|
|
|
|
return useQuery<FeaturedStatus>({
|
|
queryKey: ['featured-verification', entityCoord, featuredUntil],
|
|
queryFn: async (c) => {
|
|
// Not featured at all.
|
|
if (!claimed || !entityCoord) return 'none';
|
|
|
|
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(6000)]);
|
|
|
|
try {
|
|
// Only a receipt AUTHORED by the platform's Lightning provider is
|
|
// trustworthy. Resolve it; if unavailable we can't verify → claimed.
|
|
const providerPubkey = await getZapProviderPubkey(PRO_LIGHTNING_ADDRESS).catch(() => null);
|
|
if (!providerPubkey) return 'claimed';
|
|
|
|
// Provider-authored zap receipts paying the platform that reference this
|
|
// entity. `authors` is the critical anti-forgery constraint.
|
|
const receipts = await nostr.query(
|
|
[
|
|
{
|
|
kinds: [9735],
|
|
authors: [providerPubkey],
|
|
'#a': [entityCoord],
|
|
'#p': PLATFORM_OWNER_PUBKEYS,
|
|
limit: 20,
|
|
},
|
|
],
|
|
{ signal },
|
|
);
|
|
|
|
// A provider-authored matching receipt authorizes featured status.
|
|
if (receipts.length > 0) return 'verified';
|
|
} catch {
|
|
// Relay error — fall through to claimed.
|
|
}
|
|
|
|
// Featured tag present but unverifiable → self-attested.
|
|
return 'claimed';
|
|
},
|
|
enabled: claimed && !!entityCoord,
|
|
staleTime: 5 * 60 * 1000, // 5 min
|
|
gcTime: 30 * 60 * 1000,
|
|
retry: 1,
|
|
});
|
|
}
|