From 53cad29c7e322ecc2526769f82ed2384b7b7d091 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sat, 31 Oct 2026 17:17:08 +0700 Subject: [PATCH] fix: gate Amber inbox decryption and expire redirect ops --- src/components/auth/AmberCallbackHandler.tsx | 3 +- src/hooks/useDirectMessages.ts | 10 +++--- src/i18n/en.ts | 3 ++ src/i18n/es.ts | 3 ++ src/lib/amber.test.ts | 25 +++++++++++++ src/lib/amber.ts | 26 +++++++++++--- src/pages/AmberCallbackPage.tsx | 2 +- src/pages/DirectMessagesPage.tsx | 37 +++++++++++++++++--- 8 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 src/lib/amber.test.ts diff --git a/src/components/auth/AmberCallbackHandler.tsx b/src/components/auth/AmberCallbackHandler.tsx index 89f057a..15d9e4d 100644 --- a/src/components/auth/AmberCallbackHandler.tsx +++ b/src/components/auth/AmberCallbackHandler.tsx @@ -180,7 +180,8 @@ export function AmberCallbackHandler() { case 'nip04_decrypt': case 'nip44_decrypt': { // Store encryption/decryption results - sessionStorage.setItem('amber_crypto_result', result); + // The result is delivered through the callback promise/flow; do not + // leave private plaintext in browser storage. clearAmberPendingOperation(); toast({ title: t('settingsUi.amberCallback.operationCompleteTitle'), diff --git a/src/hooks/useDirectMessages.ts b/src/hooks/useDirectMessages.ts index e57a5ba..ea96d25 100644 --- a/src/hooks/useDirectMessages.ts +++ b/src/hooks/useDirectMessages.ts @@ -738,7 +738,8 @@ export function useConversation(otherPubkey: string) { /** * Hook to fetch all conversations */ -export function useConversations() { +export function useConversations(options: { enabled?: boolean } = {}) { + const callerEnabled = options.enabled ?? true; const { nostr } = useNostr(); const { user } = useCurrentUser(); const { logins } = useNostrLogin(); @@ -843,7 +844,7 @@ export function useConversations() { ); return { conversations: sorted, undecryptable, wraps: uniqueEvents.length }; }, - enabled: nip44Available, + enabled: nip44Available && callerEnabled, ...amberSafeQueryOptions(isInteractiveSigner(user, logins)), }); } @@ -860,7 +861,8 @@ export function useConversations() { * the protocols to adopt if those guarantees are required later. * See GROUP_MESSAGING.md for the full decision and upgrade paths. */ -export function useGroupConversations() { +export function useGroupConversations(options: { enabled?: boolean } = {}) { + const callerEnabled = options.enabled ?? true; const { nostr } = useNostr(); const { user } = useCurrentUser(); const { logins } = useNostrLogin(); @@ -943,7 +945,7 @@ export function useGroupConversations() { (a, b) => (b.lastMessage?.createdAt || 0) - (a.lastMessage?.createdAt || 0), ); }, - enabled: nip44Available, + enabled: nip44Available && callerEnabled, ...amberSafeQueryOptions(isInteractiveSigner(user, logins)), }); } diff --git a/src/i18n/en.ts b/src/i18n/en.ts index c5b760b..50fa1f8 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -4093,6 +4093,9 @@ socialUi: { runBy: 'Coordinator run by', }, dms: { + amberUnlockTitle: 'Unlock encrypted messages', + amberUnlockBody: 'Amber asks for approval for each message it decrypts. Load recent conversations only when you are ready — opening a thread may still ask a few times.', + amberUnlockCta: 'Unlock messages', securityReminder: 'Security reminder', phishingStrong: 'SkateSpots will never DM you', phishingRest: "asking to verify your account, share your keys, or send payments. If someone claims to be SkateSpots support via DM — it's a scam.", diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 6212a9e..b53f178 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -4093,6 +4093,9 @@ socialUi: { runBy: 'Coordinador gestionado por', }, dms: { + amberUnlockTitle: 'Desbloquear mensajes cifrados', + amberUnlockBody: 'Amber pide aprobación por cada mensaje que descifra. Carga las conversaciones recientes cuando quieras; abrir un hilo aún puede pedirte varias aprobaciones.', + amberUnlockCta: 'Desbloquear mensajes', securityReminder: 'Recordatorio de seguridad', phishingStrong: 'SkateSpots nunca te enviará un MD', phishingRest: 'pidiéndote que verifiques tu cuenta, compartas tus claves o envíes pagos. Si alguien dice ser soporte de SkateSpots por MD — es una estafa.', diff --git a/src/lib/amber.test.ts b/src/lib/amber.test.ts new file mode 100644 index 0000000..db8f438 --- /dev/null +++ b/src/lib/amber.test.ts @@ -0,0 +1,25 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { getAmberPendingOperation, setAmberPendingOperation } from './amber'; + +describe('Amber pending redirect operations', () => { + afterEach(() => { + vi.restoreAllMocks(); + localStorage.clear(); + sessionStorage.clear(); + }); + + it('accepts a fresh operation', () => { + setAmberPendingOperation({ type: 'get_public_key', id: 'fresh', timestamp: Date.now() }); + expect(getAmberPendingOperation()?.id).toBe('fresh'); + }); + + it('rejects and clears a stale callback operation', () => { + setAmberPendingOperation({ + type: 'get_public_key', + id: 'stale', + timestamp: Date.now() - 11 * 60 * 1000, + }); + expect(getAmberPendingOperation()).toBeNull(); + expect(localStorage.getItem('amber_pending_operation')).toBeNull(); + }); +}); diff --git a/src/lib/amber.ts b/src/lib/amber.ts index 9fc015c..90d569d 100644 --- a/src/lib/amber.ts +++ b/src/lib/amber.ts @@ -23,6 +23,8 @@ export interface AmberPendingOperation { const AMBER_PENDING_KEY = 'amber_pending_operation'; const AMBER_LOGIN_KEY = 'amber_login'; +/** Pending redirect operations are short-lived; never accept a stale callback. */ +const AMBER_PENDING_TTL_MS = 10 * 60 * 1000; /** * Store pending operation info for when Amber returns. @@ -63,6 +65,10 @@ export function setAmberPendingOperation(operation: AmberPendingOperation): void } } +function isExpiredPending(op: AmberPendingOperation | null): boolean { + return !!op && (!Number.isFinite(op.timestamp) || Date.now() - op.timestamp > AMBER_PENDING_TTL_MS); +} + /** * Get pending operation from any available storage. */ @@ -81,7 +87,12 @@ export function getAmberPendingOperation(): AmberPendingOperation | null { if (!stored) return null; try { - return JSON.parse(stored) as AmberPendingOperation; + const parsed = JSON.parse(stored) as AmberPendingOperation; + if (isExpiredPending(parsed)) { + clearAmberPendingOperation(); + return null; + } + return parsed; } catch { return null; } @@ -117,7 +128,13 @@ export async function getAmberPendingOperationAsync(): Promise @@ -430,6 +433,28 @@ export default function DirectMessagesPage() { } /> + {interactive && !amberInboxUnlocked && ( + + + +
+

{t('socialUi.dms.amberUnlockTitle')}

+

+ {t('socialUi.dms.amberUnlockBody')} +

+ +
+
+
+ )} + {/* Cordn private rooms — community + each crew's MLS chat */}

@@ -465,6 +490,8 @@ export default function DirectMessagesPage() {

+ {inboxEnabled && ( + <> {/* Group conversations (NIP-17 group DMs) */} {groups && groups.length > 0 && (
@@ -536,6 +563,8 @@ export default function DirectMessagesPage() {

)}
+ + )} {/* New Message — searchable people picker */}