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 )}