diff options
| author | Adam Soltys <[email protected]> | 2026-10-09 23:54:43 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-09 23:54:43 -0700 |
| commit | a70f34f2a5d35352f99bae8f10cbab12637e08ee (patch) | |
| tree | 7a5b83fd62d0737f4b856b04c2405ba440c39e63 /src/features/messages.js | |
| parent | 3d6bf81a19ca080d3710893b1e65f5aa3f01315f (diff) | |
| download | coinosv3-a70f34f2a5d35352f99bae8f10cbab12637e08ee.tar.gz coinosv3-a70f34f2a5d35352f99bae8f10cbab12637e08ee.zip | |
Messages: Amber users can read their DMs. Signed in with Amber by intent (NIP-55), nothing sealed to the login npub was ever decrypted — each decrypt would be a trip through the app — so isolabellart's thread with Adam showed only what that phone had sent. Worse, decryptorsComplete() counted the Amber signer as complete, so those wraps were tried on the wallet key alone and logged as opened, hidden for good. Now an interactive signer counts as incomplete (wraps wait, uncounted); Messages and the DM thread say "messages sent to you can't be opened here" with Connect Amber, which opens a nostrconnect:// link for the SAME account over NIP-46 (Amber's bunker mode; a different npub is refused, a callbackUrl reload resumes the handshake). Once a signer that can decrypt takes over, a wallet that ran with Amber re-runs the heal pass over every wrap the relays hold, so messages the old code wrote off come back. tools/amber-dm-upgrade-test.js (plays the remote signer over a shimmed relay).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
Diffstat (limited to 'src/features/messages.js')
| -rw-r--r-- | src/features/messages.js | 40 |
1 files changed, 36 insertions, 4 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 06371cf..597cb23 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -2898,11 +2898,28 @@ export function messagesFeature(ctx) { // "nothing to wait for": counting those passes gave every wrap sealed to // the login npub up after ~40 s with only the wallet key tried, and the // wrap log then hid it from every later session (a phone kept 9 of 186 - // messages). An interactive signer (Amber) is live from the moment it - // resumes and never decrypts here, so it counts as complete. + // messages). An interactive signer (Amber by intent) never decrypts here + // either, so it is incomplete too: counted as complete, every wrap sealed + // to the login npub was given up on the wallet key and logged as opened + // — an Amber user's threads held only what that device had sent. function decryptorsComplete() { const login = hook('nostrLoginIdentity'); - return !login || !!(login.signer && (login.signer.interactive || login.signer.decryptFrom)); + return !login || !!(login.signer && !login.signer.interactive && login.signer.decryptFrom); + } + const interactiveLogin = () => { const l = hook('nostrLoginIdentity'); return !!(l && l.signer && l.signer.interactive); }; + // Wraps logged while an interactive signer stood in for the login npub + // may be messages no key here ever tried. Once a signer that can decrypt + // takes over (Amber reconnected over NIP-46), the heal pass re-opens + // every wrap the relays still hold. + function noteSignerGap() { + const s = st(); + if (interactiveLogin()) { if (!s.dmSignerGap) { s.dmSignerGap = true; save(s); } return; } + if (!s.dmSignerGap || !decryptorsComplete()) return; + delete s.dmSignerGap; + s.dmHeal = 0; + save(s); + wrapLog = null; healed.clear(); seenWraps.clear(); + if (dmStarted) resubscribeStreams(); } // Raw-key unwraps (seed-derived / pasted nsec — the common case) run in @@ -3055,6 +3072,7 @@ export function messagesFeature(ctx) { const pks = dmSubKeys(); if (!pks.length) return; dmStarted = true; + if (interactiveLogin()) noteSignerGap(); loadSeenWraps(); // warm threads from the local cache. A self-thread of our own messages is // the residue of the old sent-copy misthreading (each welcome DM landed @@ -12140,6 +12158,17 @@ export function messagesFeature(ctx) { }, ui.msgReconnecting ? h('span', { class: 'spinner sm' }) : t('msgReconnect'))); } + // Amber by intent signs, but each decrypt would be a trip through the app, + // so nothing sent TO the login npub is opened here — a thread shows only + // what this device sent. Amber also speaks NIP-46; connected that way it + // decrypts quietly, and the messages held back open. + function amberDmNotice() { + if (wallet.watchOnly || !interactiveLogin()) return null; + return h('div', { class: 'row gap6 chat-signer-off', style: 'align-items:center' }, + h('span', { class: 'small muted grow' }, t('msgAmberDms')), + h('button', { class: 'btn-sm', onClick: () => hook('nostrSignerUpgrade') }, t('msgAmberConnect'))); + } + // Enter sends; Shift+Enter (the textarea's native edit) and Ctrl+J insert // a newline. Height follows the text as it's typed — imperatively, since // typing doesn't re-render — with rows as the re-render fallback. @@ -12402,6 +12431,8 @@ export function messagesFeature(ctx) { // that quietly won't open are listed. const lockedRow = signerNotice(); if (lockedRow && wallet.watchOnly) kids.push(lockedRow); + const amberRow = amberDmNotice(); + if (amberRow) kids.push(amberRow); if (pendingLink && pendingLink.where !== 'communities') kids.push(linkInviteCard()); for (const [rid, inv] of pendingDirect) kids.push(directInviteCard(rid, inv)); @@ -13319,6 +13350,7 @@ export function messagesFeature(ctx) { h('div', { class: 'chat-time' }, timeLabel(m.rumor.created_at * 1000))))) : [h('div', { class: 'muted small', style: 'text-align:center;padding:24px 0' }, t('msgNoDmsYet'))])), dmReplyBar(), + amberDmNotice(), composer(t('msgDmPlaceholder'), () => sendDM(peer), null, 'dm:' + peer, (f) => sendDMFile(peer, f)), ui.msgSheet ? dmSheet(peer) : null, choice && ui.msgRoutePick ? routeSheet(peer, choice) : null); @@ -13796,7 +13828,7 @@ export function messagesFeature(ctx) { }, // nostr-login just connected or resumed a signer: wraps the backfill // couldn't open a moment ago can be opened now — drain right away. - nostrSignerLive() { scheduleDrain(0); }, + nostrSignerLive() { noteSignerGap(); scheduleDrain(0); }, // Anyone (ark's history, other features) can open a profile or render a // small clickable identity chip. showProfile(pk) { openProfile(pk); return true; }, |
