From 20cbdd364ab0f18bce60651792b617f6a6ee0306 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Fri, 10 Jul 2026 11:07:11 -0400 Subject: [PATCH] =?UTF-8?q?fix(smirk):=20sign=20first,=20grant=20Nostr=20s?= =?UTF-8?q?cope=20only=20on=20demand=20=E2=80=94=20one=20approval=20for=20?= =?UTF-8?q?returning=20users?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously always called getNostrPublicKey() then signNostrEvent() = two approvals every login. Now signNostrEvent() runs first; only if the wallet reports the origin lacks the Nostr scope (NOT_AUTHORIZED, thrown without a popup) do we call getNostrPublicKey() to grant it and retry. First-time still does the one-time scope grant; returning users get a single approval. (The double PASSWORD is the wallet's autoLockMinutes=0 default and is user-configurable, not dapp-controllable.) --- html/js/network/smirkAuth.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/html/js/network/smirkAuth.js b/html/js/network/smirkAuth.js index 6dbc266..354dac3 100644 --- a/html/js/network/smirkAuth.js +++ b/html/js/network/smirkAuth.js @@ -141,11 +141,6 @@ const SmirkAuth = { * @returns {Promise<{verifyData: object, pubkey: string|null}>} */ async _verifyNip98(socketId, challenge) { - // Grant this origin the Nostr scope first. Per the Smirk dapp API, getNostrPublicKey() - // IS the connection for a Nostr origin (it prompts on first use); signNostrEvent() throws - // "Origin lacks the Nostr scope — call getNostrPublicKey() first" without it. - await window.smirk.getNostrPublicKey(); - // Build the NIP-98 HTTP auth event. The wallet fills in pubkey/id/sig. const evt = { kind: 27235, @@ -158,7 +153,20 @@ const SmirkAuth = { ] }; - const signed = await window.smirk.signNostrEvent(evt); + // Sign directly. If this origin doesn't yet hold the Nostr scope, the wallet throws + // NOT_AUTHORIZED (no popup); grant it once via getNostrPublicKey() and retry. Signing + // first means a RETURNING user (scope already granted) gets a SINGLE approval instead of + // two. The wallet still asks per-signature for kind 27235 — that can't be silenced. + let signed; + try { + signed = await window.smirk.signNostrEvent(evt); + } catch (scopeErr) { + const needsScope = (scopeErr && scopeErr.code === 'NOT_AUTHORIZED') + || /nostr scope|getNostrPublicKey/i.test((scopeErr && scopeErr.message) || ''); + if (!needsScope) throw scopeErr; + await window.smirk.getNostrPublicKey(); // grant the Nostr scope (one-time per origin) + signed = await window.smirk.signNostrEvent(evt); + } if (!signed || typeof signed !== 'object') { throw new Error('Smirk wallet did not return a signed nostr event'); }