aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/nostrlogin.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-09 23:54:43 -0700
committerAdam Soltys <[email protected]>2026-10-09 23:54:43 -0700
commita70f34f2a5d35352f99bae8f10cbab12637e08ee (patch)
tree7a5b83fd62d0737f4b856b04c2405ba440c39e63 /src/features/nostrlogin.js
parent3d6bf81a19ca080d3710893b1e65f5aa3f01315f (diff)
downloadcoinosv3-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/nostrlogin.js')
-rw-r--r--src/features/nostrlogin.js41
1 files changed, 35 insertions, 6 deletions
diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js
index 306a12f..2023931 100644
--- a/src/features/nostrlogin.js
+++ b/src/features/nostrlogin.js
@@ -278,6 +278,16 @@ export function nostrLoginFeature(ctx) {
// approval and the login finishes on its own. Deferred a beat so the boot
// restore has decided whether this tab is even logged out.
setTimeout(() => {
+ // the same, for a wallet that was reconnecting its own account (Amber
+ // moving to NIP-46 so messages can be read): back to that screen
+ const again = !nc && wallet.mnemonic && load().pubkey && nostrConnectPending();
+ if (again) {
+ ui.nostrReconnect = 'upgrade';
+ ui.nostrConnectOpen = true;
+ startNostrConnect(reconnectRun, again);
+ render();
+ return;
+ }
const pending = !nc && !wallet.mnemonic && ui.screen === 'unlock' && nostrConnectPending();
if (!pending) return;
if (ui.onb && ['welcome', 'signin'].includes(ui.onb.step)) ui.onb = { step: 'signin' };
@@ -676,6 +686,11 @@ export function nostrLoginFeature(ctx) {
linkSection());
}
+ const reconnectRun = (makeSigner) => reconnectSigner(makeSigner).then(() => {
+ // an upgrade is done only once the new signer can decrypt
+ if (liveSigner() && !(ui.nostrReconnect === 'upgrade' && live.interactive)) { ui.nostrReconnect = null; ui.nostrConnectOpen = false; render(); }
+ });
+
// The reconnect takeover: opened by any feature whose action just found
// the signer missing (a reload dropped it; Amethyst and friends can't
// reattach silently). Same guarded buttons as the settings card — an
@@ -683,15 +698,18 @@ export function nostrLoginFeature(ctx) {
// refuses a different account.
function reconnectScreen() {
const st = load();
- const run = (makeSigner) => reconnectSigner(makeSigner).then(() => {
- if (liveSigner()) { ui.nostrReconnect = null; render(); }
- });
+ // 'upgrade': Amber by intent is live but can't read messages — offer
+ // only its NIP-46 door (nostrconnect), same account enforced
+ const upgrade = ui.nostrReconnect === 'upgrade';
+ const run = reconnectRun;
+ // straight to the link: the screen exists for this one door
+ if (upgrade && ui.nostrUpgradeStart) { ui.nostrUpgradeStart = false; ui.nostrConnectOpen = true; startNostrConnect(run); }
return h('div', { class: 'col', style: 'gap:16px' },
ctx.brandHeader(false),
h('div', { class: 'card col', style: 'gap:12px' },
- h('h3', { style: 'margin:0' }, t('nlReconnectTitle')),
- h('p', { class: 'small muted', style: 'margin:0' }, t('nlReconnectBody', { npub: (npubOf(st.pubkey) || '').slice(0, 12) })),
- signerButtons(run, { reconnectVia: reconnectViaOf(st) }),
+ h('h3', { style: 'margin:0' }, t(upgrade ? 'nlAmberUpgradeTitle' : 'nlReconnectTitle')),
+ h('p', { class: 'small muted', style: 'margin:0' }, upgrade ? t('nlAmberUpgradeBody') : t('nlReconnectBody', { npub: (npubOf(st.pubkey) || '').slice(0, 12) })),
+ upgrade ? nostrConnectSection(run) : signerButtons(run, { reconnectVia: reconnectViaOf(st) }),
ui.nostrLoginError ? h('div', { class: 'notice err' }, ui.nostrLoginError) : null),
h('button', { class: 'btn-ghost btn-block', onClick: () => {
stopNostrConnect();
@@ -710,6 +728,17 @@ export function nostrLoginFeature(ctx) {
// An action needed the login signer and it isn't there: open the
// reconnect screen (and try the silent extension reattach in the
// background — it closes the screen by itself when it lands).
+ // Messages found an Amber-by-intent login that can't decrypt: offer to
+ // reconnect the same account over NIP-46.
+ nostrSignerUpgrade() {
+ const st = load();
+ if (!st.pubkey) return false;
+ ui.nostrReconnect = 'upgrade';
+ ui.nostrUpgradeStart = true;
+ ui.nostrLoginError = '';
+ render();
+ return true;
+ },
nostrReconnectPrompt() {
const st = load();
if (!st.pubkey || liveSigner()) return false;