From 48b127f7b0dd7eafbb772704ed5ccbe12c047ba3 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Mon, 7 Sep 2026 20:08:39 +0700 Subject: [PATCH] Amber UX: honest multi-approval hint for encrypted chat/DM sends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit outcome for the two suspected "double-prompt" Amber flows: • Image upload — CONFIRMED already a single signature. BlossomUploader (even at the pinned 0.47.1) signs one kind-24242 auth event and reuses it across all servers via Promise.any, so uploads = 1 Amber prompt. No change needed. • DM / crew-chat send — the multiple prompts are protocol-inherent and MUST NOT be optimized away: NIP-17 seals+signs once per recipient (and to self); a Cordn crew message signs an author proof then encrypts once per roster member. Reducing the count would weaken confidentiality or authorship. So instead of a code shortcut we add transparency. New : a tiny heads-up shown ONLY to interactive (Amber) signers before an inherently multi-approval action, telling them how many Approve screens to expect and that it's normal. Renders nothing for silent signers (extension/nsec/bunker), where the ops are invisible. Wired into: • CrewChat composer — count derived from transport + roster size (Cordn: roster+1; NIP-17: roster*2). • ConversationPage (1:1 DM) composer — 4 approvals (encrypt+sign to the recipient and to yourself). Dependencies: reviewed the full manifest; deliberately left unchanged. Every upgrade candidate is either on the critical crypto/Nostr/payments path (nostrify, nostr-tools, cashu, bitcoinjs, noble, scure — pinned for safety) or a documented major bump (lucide v1, react 19, react-router 7) with breaking changes and no user-facing benefit. No security advisories force a change, and caret ranges already pull safe patch/minor updates. "It must work" wins over churn. Bumps to 1.2.20 (Android versionCode 10220 auto-derived). Co-authored-by: shakespeare.diy --- package.json | 2 +- src/components/AmberApprovalHint.tsx | 64 ++++++++++++++++++++++++++++ src/components/CrewChat.tsx | 14 ++++++ src/pages/ConversationPage.tsx | 4 ++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/components/AmberApprovalHint.tsx diff --git a/package.json b/package.json index 18de607..ba99631 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "skate-spots", "private": true, - "version": "1.2.19", + "version": "1.2.20", "type": "module", "homepage": "https://skate-spots.com", "scripts": { diff --git a/src/components/AmberApprovalHint.tsx b/src/components/AmberApprovalHint.tsx new file mode 100644 index 0000000..225f28f --- /dev/null +++ b/src/components/AmberApprovalHint.tsx @@ -0,0 +1,64 @@ +import { KeyRound } from 'lucide-react'; + +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useNostrLogin } from '@nostrify/react/login'; +import { isInteractiveSigner } from '@/lib/signerCapabilities'; +import { cn } from '@/lib/utils'; + +interface AmberApprovalHintProps { + /** + * How many separate signer approvals this action will require. When known and + * > 1 it's shown to set expectations (e.g. an encrypted group message signs + * once per recipient). Omit for a generic "may ask you to approve" note. + */ + approvals?: number; + /** Short label for the action, e.g. "send this message". */ + action?: string; + className?: string; +} + +/** + * A tiny, honest heads-up shown ONLY to interactive-signer (Amber) users before + * an action that is inherently multi-approval. + * + * Some flows are cryptographically obliged to hit the signer several times — a + * NIP-17 DM seals + signs once per recipient (and once to self); an encrypted + * crew message signs an author proof then encrypts once per member. With a + * silent signer (extension / nsec / bunker) these are instant and invisible, so + * there's nothing to warn about. With Amber, each op is a foreground Approve + * screen, so without this the user sees Amber "pop up several times" for one + * Send and reasonably assumes something is broken. + * + * We DON'T try to reduce the count (that would weaken the encryption/authorship + * model) — we just tell the truth up front. Renders nothing for silent signers. + */ +export function AmberApprovalHint({ approvals, action = 'send', className }: AmberApprovalHintProps) { + const { user } = useCurrentUser(); + const { logins } = useNostrLogin(); + + if (!isInteractiveSigner(user, logins)) return null; + + const multi = typeof approvals === 'number' && approvals > 1; + + return ( +

+ + + {multi ? ( + <> + Your signer will ask you to approve{' '} + {approvals} times to {action} — + that's normal for encrypted messages (one approval per recipient). Approve each. + + ) : ( + <>Your signer will ask you to approve to {action}. + )} + +

+ ); +} diff --git a/src/components/CrewChat.tsx b/src/components/CrewChat.tsx index 5cf6f90..7585c7e 100644 --- a/src/components/CrewChat.tsx +++ b/src/components/CrewChat.tsx @@ -14,6 +14,7 @@ import { Skeleton } from '@/components/ui/skeleton'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useNostrLogin } from '@nostrify/react/login'; import { hasNip44 as checkNip44 } from '@/lib/signerCapabilities'; +import { AmberApprovalHint } from '@/components/AmberApprovalHint'; import { useAuthor } from '@/hooks/useAuthor'; import { useToast } from '@/hooks/useToast'; import { @@ -249,6 +250,14 @@ export function CrewChat({ const canSendNip44 = checkNip44(user, logins); + // How many signer approvals one Send will cost — used to warn Amber users so + // the repeated Approve screens don't look like a bug. Roster = founder + all + // members (deduped, includes self). Cordn v2: one author-proof signature plus + // one encryption per roster member. NIP-17: each target (others + self) is an + // encrypt + a seal signature, i.e. two ops per member. + const rosterSize = Math.max(participants.length, 1); + const approvalCount = usingCordn ? rosterSize + 1 : rosterSize * 2; + const handleSend = async (e: React.FormEvent) => { e.preventDefault(); const text = draft.trim(); @@ -427,6 +436,11 @@ export function CrewChat({ {isPending ? : } + )} diff --git a/src/pages/ConversationPage.tsx b/src/pages/ConversationPage.tsx index 3ef7da4..668e4cb 100644 --- a/src/pages/ConversationPage.tsx +++ b/src/pages/ConversationPage.tsx @@ -25,6 +25,7 @@ import { RefreshCw, } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { AmberApprovalHint } from '@/components/AmberApprovalHint'; import NotFound from './NotFound'; // Single message bubble @@ -294,6 +295,9 @@ export default function ConversationPage() {

Messages are end-to-end encrypted using NIP-17

+ {/* A 1:1 NIP-17 DM encrypts + signs to the recipient AND to yourself + (so you keep a copy) = up to 4 signer approvals. Warn Amber users. */} +