feat: enhance Amber login flow with unsigned event management

- Updated the AuthModal to reset the amberUnsignedEvent state during various phases of the login process.
- Improved handling of the unsigned NIP-98 event for better reuse during signing.
- Enhanced error handling to ensure the correct event data is used when authenticating with Amber.

These changes streamline the Amber login experience and improve state management for the signing process.
This commit is contained in:
Dorian
2026-02-14 11:25:27 +00:00
parent ca3d390180
commit 40485e9622

View File

@@ -117,7 +117,7 @@
Continue
</button>
<button
@click="amberPhase = 'idle'; amberPubkey = null"
@click="amberPhase = 'idle'; amberPubkey = null; amberUnsignedEvent = null"
class="w-full text-center text-sm text-white/40 hover:text-white/60 transition-colors"
>
Cancel
@@ -141,7 +141,7 @@
Complete Sign-in
</button>
<button
@click="amberPhase = 'idle'; amberPubkey = null"
@click="amberPhase = 'idle'; amberPubkey = null; amberUnsignedEvent = null"
class="w-full text-center text-sm text-white/40 hover:text-white/60 transition-colors"
>
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<string | null>(null)
/** The exact unsigned NIP-98 event sent to Amber for signing (reused in Phase 3) */
const amberUnsignedEvent = ref<any>(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) {