feat(app): add Nostr identity login via NIP-07 browser extension
Create useNostrIdentity.ts composable with NIP-07 window.nostr API integration (getPublicKey, signEvent, extension detection). Add npub bech32 encoding to bech32.ts. Create NostrLogin.vue settings component with login/logout, npub display with copy, and extension install guidance. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
71125bfde9
commit
685d357573
@@ -0,0 +1,119 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { encodeNpub } from '@/utils/bech32'
|
||||
|
||||
/**
|
||||
* NIP-07 browser extension interface (nos2x, Alby, etc.)
|
||||
*/
|
||||
interface NostrExtension {
|
||||
getPublicKey(): Promise<string>
|
||||
signEvent(event: UnsignedNostrEvent): Promise<SignedNostrEvent>
|
||||
getRelays?(): Promise<Record<string, { read: boolean; write: boolean }>>
|
||||
nip04?: {
|
||||
encrypt(pubkey: string, plaintext: string): Promise<string>
|
||||
decrypt(pubkey: string, ciphertext: string): Promise<string>
|
||||
}
|
||||
}
|
||||
|
||||
export interface UnsignedNostrEvent {
|
||||
kind: number
|
||||
created_at: number
|
||||
tags: string[][]
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface SignedNostrEvent extends UnsignedNostrEvent {
|
||||
id: string
|
||||
pubkey: string
|
||||
sig: string
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
nostr?: NostrExtension
|
||||
}
|
||||
}
|
||||
|
||||
const pubkey = ref<string | null>(null)
|
||||
const isAvailable = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
export function useNostrIdentity() {
|
||||
const npub = computed(() => {
|
||||
if (!pubkey.value) return null
|
||||
try {
|
||||
return encodeNpub(pubkey.value)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const isLoggedIn = computed(() => !!pubkey.value)
|
||||
|
||||
const truncatedNpub = computed(() => {
|
||||
if (!npub.value) return null
|
||||
return npub.value.slice(0, 12) + '...' + npub.value.slice(-8)
|
||||
})
|
||||
|
||||
function checkAvailability() {
|
||||
isAvailable.value = typeof window !== 'undefined' && !!window.nostr
|
||||
}
|
||||
|
||||
async function login(): Promise<void> {
|
||||
error.value = null
|
||||
if (!window.nostr) {
|
||||
error.value = 'No Nostr extension detected. Install nos2x, Alby, or another NIP-07 extension.'
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const pk = await window.nostr.getPublicKey()
|
||||
pubkey.value = pk
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to get public key'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function signEvent(event: UnsignedNostrEvent): Promise<SignedNostrEvent | null> {
|
||||
error.value = null
|
||||
if (!window.nostr) {
|
||||
error.value = 'No Nostr extension detected'
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return await window.nostr.signEvent(event)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to sign event'
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
pubkey.value = null
|
||||
error.value = null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkAvailability()
|
||||
// Some extensions load async — recheck after a short delay
|
||||
setTimeout(checkAvailability, 500)
|
||||
})
|
||||
|
||||
return {
|
||||
pubkey,
|
||||
npub,
|
||||
isAvailable,
|
||||
isLoggedIn,
|
||||
isLoading,
|
||||
error,
|
||||
truncatedNpub,
|
||||
login,
|
||||
logout,
|
||||
signEvent,
|
||||
checkAvailability,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user