Amber UX: honest multi-approval hint for encrypted chat/DM sends
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 <AmberApprovalHint>: 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 <assistant@shakespeare.diy>
This commit is contained in:
parent
56f29e0a29
commit
48b127f7b0
4 changed files with 83 additions and 1 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "skate-spots",
|
"name": "skate-spots",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.19",
|
"version": "1.2.20",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"homepage": "https://skate-spots.com",
|
"homepage": "https://skate-spots.com",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
64
src/components/AmberApprovalHint.tsx
Normal file
64
src/components/AmberApprovalHint.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
'flex items-start gap-1.5 text-[11px] leading-snug text-muted-foreground',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<KeyRound className="mt-0.5 h-3 w-3 shrink-0 text-amber-400" />
|
||||||
|
<span>
|
||||||
|
{multi ? (
|
||||||
|
<>
|
||||||
|
Your signer will ask you to approve{' '}
|
||||||
|
<strong className="font-semibold text-amber-300">{approvals} times</strong> to {action} —
|
||||||
|
that's normal for encrypted messages (one approval per recipient). Approve each.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>Your signer will ask you to approve to {action}.</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||||
import { useNostrLogin } from '@nostrify/react/login';
|
import { useNostrLogin } from '@nostrify/react/login';
|
||||||
import { hasNip44 as checkNip44 } from '@/lib/signerCapabilities';
|
import { hasNip44 as checkNip44 } from '@/lib/signerCapabilities';
|
||||||
|
import { AmberApprovalHint } from '@/components/AmberApprovalHint';
|
||||||
import { useAuthor } from '@/hooks/useAuthor';
|
import { useAuthor } from '@/hooks/useAuthor';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import {
|
import {
|
||||||
|
|
@ -249,6 +250,14 @@ export function CrewChat({
|
||||||
|
|
||||||
const canSendNip44 = checkNip44(user, logins);
|
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) => {
|
const handleSend = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const text = draft.trim();
|
const text = draft.trim();
|
||||||
|
|
@ -427,6 +436,11 @@ export function CrewChat({
|
||||||
{isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
{isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
<AmberApprovalHint
|
||||||
|
approvals={approvalCount}
|
||||||
|
action="send to your crew"
|
||||||
|
className="mt-2"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import {
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { AmberApprovalHint } from '@/components/AmberApprovalHint';
|
||||||
import NotFound from './NotFound';
|
import NotFound from './NotFound';
|
||||||
|
|
||||||
// Single message bubble
|
// Single message bubble
|
||||||
|
|
@ -294,6 +295,9 @@ export default function ConversationPage() {
|
||||||
<p className="text-[10px] sm:text-xs text-gray-600 mt-1.5 sm:mt-2 text-center hidden sm:block">
|
<p className="text-[10px] sm:text-xs text-gray-600 mt-1.5 sm:mt-2 text-center hidden sm:block">
|
||||||
Messages are end-to-end encrypted using NIP-17
|
Messages are end-to-end encrypted using NIP-17
|
||||||
</p>
|
</p>
|
||||||
|
{/* 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. */}
|
||||||
|
<AmberApprovalHint approvals={4} action="send this message" className="mt-1.5 justify-center" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue