diff --git a/src/components/AuthModal.vue b/src/components/AuthModal.vue index 8ac39b8..90b7504 100644 --- a/src/components/AuthModal.vue +++ b/src/components/AuthModal.vue @@ -117,7 +117,7 @@ Continue Cancel @@ -141,7 +141,7 @@ Complete Sign-in Cancel @@ -257,6 +257,8 @@ const isLoading = computed(() => authLoading.value || sovereignGenerating.value) // Amber three-phase login state const amberPhase = ref<'idle' | 'waiting-pubkey' | 'waiting-signature'>('idle') const amberPubkey = ref(null) +/** The exact unsigned NIP-98 event sent to Amber for signing (reused in Phase 3) */ +const amberUnsignedEvent = ref(null) // ── Sovereign Identity Flow ────────────────────────────────────── type SovereignPhase = 'normal' | 'nah' | 'own-privacy' | 'generated' @@ -275,6 +277,7 @@ watch(() => props.isOpen, (open) => { generatedKeys.value = null errorMessage.value = null amberPhase.value = 'idle' + amberUnsignedEvent.value = null } }) @@ -283,6 +286,7 @@ function closeModal() { errorMessage.value = null amberPhase.value = 'idle' amberPubkey.value = null + amberUnsignedEvent.value = null sovereignPhase.value = 'normal' sovereignDismissed.value = false generatedKeys.value = null @@ -499,7 +503,9 @@ async function handleAmberPubkeyRead() { accountManager.addAccount(account) accountManager.setActive(account) - // Build the unsigned NIP-98 event that the backend expects + // Build the unsigned NIP-98 event that the backend expects. + // Store it so Phase 3 can reuse the exact same event data + // (same created_at) when combining with the returned signature. const sessionUrl = authService.getNostrSessionUrl() const unsignedEvent = { kind: 27235, @@ -511,6 +517,7 @@ async function handleAmberPubkeyRead() { content: '', pubkey, } + amberUnsignedEvent.value = unsignedEvent // Open Amber to sign this NIP-98 event const eventJson = JSON.stringify(unsignedEvent) @@ -545,52 +552,62 @@ async function handleAmberSignComplete() { throw new Error('Clipboard is empty. Make sure you approved the signing request in Amber.') } - // Amber may return the full signed event JSON or just the signature hex - let authHeader: string - - if (clipboardText.startsWith('{')) { - // Full signed event JSON — use directly - const signedEvent = JSON.parse(clipboardText) - if (!signedEvent.sig) { - throw new Error('Signed event is missing the signature. Please try again.') - } - authHeader = `Nostr ${btoa(JSON.stringify(signedEvent))}` - } else if (/^[0-9a-f]{128}$/i.test(clipboardText)) { - // Just the signature hex — we need to reconstruct the full event - // Build the same unsigned event we sent to Amber - const pubkey = amberPubkey.value - if (!pubkey) throw new Error('Lost track of the pubkey. Please start over.') - - const sessionUrl = authService.getNostrSessionUrl() - const event = { - kind: 27235, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['u', sessionUrl], - ['method', 'POST'], - ], - content: '', - pubkey, - sig: clipboardText, - } - - // Calculate event ID using nostr-tools - const { getEventHash } = await import('nostr-tools/pure') - ;(event as any).id = getEventHash(event as any) - - authHeader = `Nostr ${btoa(JSON.stringify(event))}` - } else { - throw new Error('Clipboard does not contain a signed event. Got: ' + clipboardText.slice(0, 20) + '...') - } - const pubkey = amberPubkey.value if (!pubkey) throw new Error('Lost track of the pubkey. Please start over.') + const storedEvent = amberUnsignedEvent.value + if (!storedEvent) throw new Error('Lost track of the signing request. Please start over.') + + // Amber may return different formats depending on version: + // 1. Full signed event JSON (starts with '{') + // 2. Just the hex signature (64-byte Schnorr = 128 hex chars) + // 3. Hex string of varying length (some Amber versions) + let authHeader: string + + // Try 1: Full signed event JSON + if (clipboardText.startsWith('{')) { + try { + const signedEvent = JSON.parse(clipboardText) + if (signedEvent.sig) { + authHeader = `Nostr ${btoa(JSON.stringify(signedEvent))}` + } else { + throw new Error('missing sig') + } + } catch { + throw new Error('Clipboard contains JSON but it is not a valid signed event. Please try again.') + } + } + // Try 2: Hex string (signature) — combine with the stored unsigned event + else if (/^[0-9a-f]+$/i.test(clipboardText)) { + const sig = clipboardText + + // Reconstruct the full event using the EXACT unsigned event we + // sent to Amber (same created_at, tags, etc.) + the signature + const event = { + ...storedEvent, + sig, + } + + // Calculate the event ID + const { getEventHash } = await import('nostr-tools/pure') + event.id = getEventHash(event as any) + + authHeader = `Nostr ${btoa(JSON.stringify(event))}` + } + // Nothing we recognise + else { + throw new Error( + 'Could not read the signed event from Amber. ' + + 'Please make sure you approved the request in Amber, then try again.' + ) + } + // Authenticate with the pre-signed NIP-98 header (bypasses signer.signEvent) await loginWithNostr(pubkey, 'amber-nip55', {}, authHeader) amberPhase.value = 'idle' amberPubkey.value = null + amberUnsignedEvent.value = null emit('success') closeModal() } catch (error: any) {