fix(audit P1): comment cache key, ticket verify fails closed, featured amount check, no empty-object persist, sticky sub-bars

This commit is contained in:
SkateSpots 2026-10-05 21:07:07 +07:00
parent f2bfbf4274
commit 5728c226d5
8 changed files with 34 additions and 5 deletions

View file

@ -85,7 +85,7 @@ export function usePostComment() {
onSuccess: (_, { root }) => { onSuccess: (_, { root }) => {
// Invalidate and refetch comments // Invalidate and refetch comments
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ['comments', root instanceof URL ? root.toString() : root.id] queryKey: ['nostr', 'comments', root instanceof URL ? root.toString() : root.id]
}); });
}, },
}); });

View file

@ -6801,6 +6801,8 @@ walletUi: {
noRsvpDesc: 'The holder has no RSVP on record for this event.', noRsvpDesc: 'The holder has no RSVP on record for this event.',
admitted: 'Admitted', admitted: 'Admitted',
admittedDesc: 'Organiser’s event + the holder RSVP’d.', admittedDesc: 'Organiser’s event + the holder RSVP’d.',
noHolder: 'No holder in the link',
noHolderDesc: 'This QR has no ticket holder (`?h=`), so the holder can’t be checked.',
}, },
passes: { passes: {
tab: 'My passes', tab: 'My passes',

View file

@ -6801,6 +6801,8 @@ walletUi: {
noRsvpDesc: 'El titular no tiene confirmación registrada para este evento.', noRsvpDesc: 'El titular no tiene confirmación registrada para este evento.',
admitted: 'Admitido', admitted: 'Admitido',
admittedDesc: 'Evento del organizador + el titular confirmó.', admittedDesc: 'Evento del organizador + el titular confirmó.',
noHolder: 'Sin titular en el enlace',
noHolderDesc: 'Este QR no incluye titular (`?h=`), así que no se puede comprobar.',
}, },
passes: { passes: {
tab: 'Mis pases', tab: 'Mis pases',

View file

@ -12,9 +12,13 @@
*/ */
import { PRO_LIGHTNING_ADDRESS } from '@/lib/sasquatchPro'; import { PRO_LIGHTNING_ADDRESS } from '@/lib/sasquatchPro';
import { getZapProviderPubkey } from '@/lib/lightningAddress'; import { getZapProviderPubkey } from '@/lib/lightningAddress';
import { getSatoshisFromBolt11 } from '@/lib/bolt11';
import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions'; import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions';
import type { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import type { NostrEvent, NostrFilter } from '@nostrify/nostrify';
/** Minimum paid amount for a "featured" receipt to count (closes the 1-sat self-zap hole). */
const MIN_FEATURED_SATS = 1000;
let providerPromise: Promise<string | null> | null = null; let providerPromise: Promise<string | null> | null = null;
function cachedProvider(): Promise<string | null> { function cachedProvider(): Promise<string | null> {
@ -45,6 +49,12 @@ export async function verifiedFeaturedCoords(
}); });
const set = new Set<string>(); const set = new Set<string>();
for (const r of receipts) { for (const r of receipts) {
// Require a real payment — a receipt's mere presence let anyone self-zap
// 1 sat and buy a "Verified" badge. (The exact tier also depends on the
// expiry; a floor is enough to close the hole.)
const invoice = r.tags.find(([n]) => n === 'bolt11')?.[1];
const sats = invoice ? getSatoshisFromBolt11(invoice) : 0;
if (!Number.isFinite(sats) || sats < MIN_FEATURED_SATS) continue;
for (const [n, v] of r.tags) if (n === 'a' && v) set.add(v); for (const [n, v] of r.tags) if (n === 'a' && v) set.add(v);
} }
return set; return set;

View file

@ -103,6 +103,14 @@ export function shouldDehydrateQuery(query: Query): boolean {
// return 0 results, and caching that would wipe the last-known map on the next // return 0 results, and caching that would wipe the last-known map on the next
// cold start — the exact opposite of what persistence is for. // cold start — the exact opposite of what persistence is for.
if (Array.isArray(query.state.data) && query.state.data.length === 0) return false; if (Array.isArray(query.state.data) && query.state.data.length === 0) return false;
// Never persist an empty plain object either — e.g. `useAuthor` returns `{}`
// when every relay failed, and persisting that would overwrite a good cached
// profile on the next cold start.
if (
typeof query.state.data === 'object' &&
!Array.isArray(query.state.data) &&
Object.keys(query.state.data as Record<string, unknown>).length === 0
) return false;
const prefix = Array.isArray(query.queryKey) ? query.queryKey[0] : query.queryKey; const prefix = Array.isArray(query.queryKey) ? query.queryKey[0] : query.queryKey;
if (typeof prefix !== 'string') return false; if (typeof prefix !== 'string') return false;

View file

@ -228,7 +228,7 @@ export default function SkatersPage() {
</div> </div>
{/* ── Search + filters ── */} {/* ── Search + filters ── */}
<div className="sticky top-0 z-20 bg-black/95 backdrop-blur-md border-b border-white/10"> <div className="sticky top-14 md:top-16 z-20 bg-black/95 backdrop-blur-md border-b border-white/10">
<div className="container px-4 py-3 max-w-6xl space-y-3"> <div className="container px-4 py-3 max-w-6xl space-y-3">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />

View file

@ -1070,7 +1070,7 @@ export default function StashPage() {
`}</style> `}</style>
{/* ── Sticky header ── */} {/* ── Sticky header ── */}
<div className="sticky top-0 z-30 bg-black/96 backdrop-blur-md border-b border-white/8"> <div className="sticky top-14 md:top-16 z-30 bg-black/96 backdrop-blur-md border-b border-white/8">
<div className="container px-4 max-w-7xl"> <div className="container px-4 max-w-7xl">
{/* Title row — wraps on phones so the actions get their own line */} {/* Title row — wraps on phones so the actions get their own line */}
<div className="flex flex-wrap items-center justify-between min-h-14 py-2 gap-x-3 gap-y-2"> <div className="flex flex-wrap items-center justify-between min-h-14 py-2 gap-x-3 gap-y-2">

View file

@ -162,8 +162,15 @@ export default function VerifyCardPage() {
else if (isPark && expiry === 'expired') { tone = 'bad'; label = t('cardUi.v.expired'); sub = t('cardUi.v.expiredDesc'); } else if (isPark && expiry === 'expired') { tone = 'bad'; label = t('cardUi.v.expired'); sub = t('cardUi.v.expiredDesc'); }
else if (isPark && venue?.status === 'mismatch') { tone = 'warn'; label = t('cardUi.v.unverifiedIssuer'); sub = t('cardUi.v.unverifiedIssuerDesc'); } else if (isPark && venue?.status === 'mismatch') { tone = 'warn'; label = t('cardUi.v.unverifiedIssuer'); sub = t('cardUi.v.unverifiedIssuerDesc'); }
else if (isPark && venue?.status === 'none') { tone = 'warn'; label = t('cardUi.v.notBound'); sub = t('cardUi.v.notBoundDesc'); } else if (isPark && venue?.status === 'none') { tone = 'warn'; label = t('cardUi.v.notBound'); sub = t('cardUi.v.notBoundDesc'); }
else if (isTicket && holderHex && ticket.data?.status === 'none') { tone = 'warn'; label = t('cardUi.v.noRsvp'); sub = t('cardUi.v.noRsvpDesc'); } else if (isTicket) {
else if (isTicket) { tone = 'good'; label = t('cardUi.v.admitted'); sub = t('cardUi.v.admittedDesc'); } // Fail CLOSED: only green when a holder is bound AND their RSVP is positive.
// Missing `?h=`, a loading/errored RSVP, or "none" must not read as admitted.
const rs = ticket.data?.status;
const confirmed = !!holderHex && (rs === 'accepted' || rs === 'going' || rs === 'tentative' || rs === 'interested');
if (confirmed) { tone = 'good'; label = t('cardUi.v.admitted'); sub = t('cardUi.v.admittedDesc'); }
else if (!holderHex) { tone = 'warn'; label = t('cardUi.v.noHolder'); sub = t('cardUi.v.noHolderDesc'); }
else { tone = 'warn'; label = t('cardUi.v.noRsvp'); sub = t('cardUi.v.noRsvpDesc'); }
}
else if (isPark) { tone = 'good'; label = t('cardUi.v.valid'); sub = t('cardUi.v.validDesc'); } else if (isPark) { tone = 'good'; label = t('cardUi.v.valid'); sub = t('cardUi.v.validDesc'); }
else { tone = 'good'; label = t('cardUi.verify.authentic'); sub = t('cardUi.verify.authenticDesc'); } else { tone = 'good'; label = t('cardUi.verify.authentic'); sub = t('cardUi.verify.authenticDesc'); }
} }