From 53c451ccfcde46d8e0be9aac09d95a01f7adef77 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Mon, 28 Sep 2026 20:16:15 +0700 Subject: [PATCH] =?UTF-8?q?fix(map+badges+sasquatch):=20attribution=20cont?= =?UTF-8?q?rast/overlap,=20dedupe=20=F0=9F=A6=A7,=20label=20AI=20balance,?= =?UTF-8?q?=20persist=20NIP-98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Map attribution: raise contrast (grey-500/80 → grey-200/90) and add `isolate` to the map container so its internal z-indexes (Leaflet panes, overlays up to z-2000) stay inside the map — the © OpenStreetMap line was painting OVER the Sasquatch panel (z-60). Panel/bubble drop to z-40 so Radix dialogs (z-50) now sit above it instead of under it. - Sasquatch header: remove the duplicate 🦧 (the avatar + context subtitle carry it), and label the owner-only chip '⚡ AI ' (it is the AI credit balance, not a wallet balance). - proAuth: persist the NIP-98 auth in localStorage for its validity window, so Amber signs once per ~11h instead of on every app load (the panel itself never signed — this is the first-message auth). --- src/components/SasquatchGuide.tsx | 10 ++--- src/lib/proAuth.ts | 63 ++++++++++++++++++++++++------- src/pages/MapPage.tsx | 12 ++++-- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/src/components/SasquatchGuide.tsx b/src/components/SasquatchGuide.tsx index 871bd59..6d8ba86 100644 --- a/src/components/SasquatchGuide.tsx +++ b/src/components/SasquatchGuide.tsx @@ -1474,7 +1474,7 @@ Instructions:
- 🦧 Sasquatch + Sasquatch {isPro ? ( - ⚡ {aiStatus.data.balanceSats.toLocaleString()} sats + ⚡ AI {aiStatus.data.balanceSats.toLocaleString()} )}
diff --git a/src/lib/proAuth.ts b/src/lib/proAuth.ts index fc4326d..54f098b 100644 --- a/src/lib/proAuth.ts +++ b/src/lib/proAuth.ts @@ -6,8 +6,9 @@ * • `x-sasquatch-auth` — a base64 NIP-98 (kind 27235) event proving the caller; * • `x-sasquatch-pro` — base64 of the Pro zap receipt proving an active pass. * - * The NIP-98 event is signed ONCE per session and reused, so an interactive - * signer (Amber) doesn't pop on every message. + * The NIP-98 event is signed ONCE and reused for its whole validity window — + * persisted to localStorage so an interactive signer (Amber) does not pop again + * on every app load. (It expires with the 12 h window the proxy enforces.) */ import type { NostrEvent } from '@nostrify/nostrify'; import { sasquatchAiBase } from '@/lib/sasquatchAi'; @@ -23,6 +24,32 @@ type SignEvent = (event: { let cachedAuthHeader: string | null = null; let cachedAuthPubkey: string | null = null; +/** Persisted NIP-98 auth (keyed to the pubkey, valid while inside the window). */ +const AUTH_STORAGE_KEY = 'skatespots:sasquatch-nip98'; +/** Reuse for 11 h to stay safely inside the proxy's 12 h acceptance window. */ +const AUTH_MAX_AGE_SECONDS = 11 * 3600; + +function loadStoredAuth(pubkey: string): string | null { + try { + const raw = localStorage.getItem(AUTH_STORAGE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw) as { pubkey?: string; header?: string; createdAt?: number }; + if (parsed?.pubkey !== pubkey || !parsed.header || !parsed.createdAt) return null; + if (Math.abs(Date.now() / 1000 - parsed.createdAt) > AUTH_MAX_AGE_SECONDS) return null; + return parsed.header; + } catch { + return null; + } +} + +function storeAuth(pubkey: string, header: string, createdAt: number): void { + try { + localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ pubkey, header, createdAt })); + } catch { + /* private mode — the in-memory cache still works for this session */ + } +} + function toBase64(value: string): string { // Event JSON is ASCII (hex pubkeys/ids, tags), so btoa is safe here. return btoa(value); @@ -39,17 +66,27 @@ export async function buildProAuthHeaders(opts: { if (!pubkey || !signEvent) return headers; if (cachedAuthPubkey !== pubkey || !cachedAuthHeader) { - const event = await signEvent({ - kind: 27235, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['u', sasquatchAiBase()], - ['method', 'POST'], - ], - content: '', - }); - cachedAuthHeader = toBase64(JSON.stringify(event)); - cachedAuthPubkey = pubkey; + // Reuse a stored signature first — otherwise Amber pops on every app load + // for a signer that would happily reuse the same NIP-98 for 12 h. + const stored = loadStoredAuth(pubkey); + if (stored) { + cachedAuthHeader = stored; + cachedAuthPubkey = pubkey; + } else { + const createdAt = Math.floor(Date.now() / 1000); + const event = await signEvent({ + kind: 27235, + created_at: createdAt, + tags: [ + ['u', sasquatchAiBase()], + ['method', 'POST'], + ], + content: '', + }); + cachedAuthHeader = toBase64(JSON.stringify(event)); + cachedAuthPubkey = pubkey; + storeAuth(pubkey, cachedAuthHeader, createdAt); + } } headers['x-sasquatch-auth'] = cachedAuthHeader; diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index f348507..154e21c 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -1603,7 +1603,11 @@ export default function MapPage() { )} {/* Main Content */} -
+ {/* `isolate` creates a stacking context so the map's internal z-indexes + (Leaflet panes, our overlays up to z-[2000]) stay INSIDE the map. Without + it they competed with the root and the © OpenStreetMap attribution + (z-999) painted on top of the Sasquatch panel (z-40). */} +
{viewMode === 'map' ? ( <> -
- © OpenStreetMap +
+ © OpenStreetMap {showBtcMap && ( - <>{' · '}BTC Map + <>{' · '}BTC Map )}