feat: NIP-98 + JWT authentication with signer support

Replace insecure raw-pubkey auth with cryptographic NIP-98 signed
requests and server-issued JWT sessions. Logout now fully clears
all state including nsec. Add yellow "Use Nostr Signer" button
for Amber/NIP-07 remote signers.

- Server: JWT middleware (HMAC-SHA256, 24h expiry), NIP-98 verification
- Server: POST /api/auth/nostr/session endpoint
- Frontend: NIP-98 token builder + authFetch wrapper with JWT Bearer
- Frontend: All authenticated API calls use authFetch
- Security: logout clears JWT, pubkey, bot, nsec, and profile pic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-09 12:31:25 +00:00
co-authored by Claude Opus 4.6
parent ad96d1158f
commit 3ba05a66b4
8 changed files with 532 additions and 105 deletions
+9 -8
View File
@@ -4,6 +4,7 @@ import { finalizeEvent } from 'nostr-tools'
import * as nip04 from 'nostr-tools/nip04'
import * as nip44 from 'nostr-tools/nip44'
import { hexToBytes, bytesToHex } from 'nostr-tools/utils'
import { authFetch } from '../lib/nostr-auth'
type WalletMethod = 'nwc' | 'lnaddress' | null
type PaymentStatus = 'idle' | 'invoiced' | 'paying' | 'confirmed' | 'failed'
@@ -59,7 +60,7 @@ export function useWallet() {
// Validate the NWC URL format
parseNwcUrl(connectionString)
const res = await fetch('/api/payments/connect-wallet', {
const res = await authFetch('/api/payments/connect-wallet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -88,7 +89,7 @@ export function useWallet() {
throw new Error('Invalid Lightning Address format')
}
const res = await fetch('/api/payments/connect-wallet', {
const res = await authFetch('/api/payments/connect-wallet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -111,7 +112,7 @@ export function useWallet() {
async function disconnectWallet(): Promise<void> {
if (!pubkey.value) return
await fetch('/api/payments/disconnect-wallet', {
await authFetch('/api/payments/disconnect-wallet', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pubkey: pubkey.value }),
@@ -128,7 +129,7 @@ export function useWallet() {
async function checkWalletStatus(): Promise<void> {
if (!pubkey.value) return
const res = await fetch(`/api/payments/wallet-status?pubkey=${pubkey.value}`)
const res = await authFetch(`/api/payments/wallet-status?pubkey=${pubkey.value}`)
if (res.ok) {
const data = await res.json()
isWalletConnected.value = data.connected
@@ -144,7 +145,7 @@ export function useWallet() {
try {
// Create invoice
const invoiceRes = await fetch('/api/payments/create-invoice', {
const invoiceRes = await authFetch('/api/payments/create-invoice', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ botId, pubkey: pubkey.value }),
@@ -175,7 +176,7 @@ export function useWallet() {
const preimage = await payViaNWC(nwcUrl, bolt11)
// Tell server payment is confirmed (skip lookup_invoice polling)
await fetch(`/api/payments/confirm/${paymentId}`, {
await authFetch(`/api/payments/confirm/${paymentId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ preimage, pubkey: pubkey.value }),
@@ -187,7 +188,7 @@ export function useWallet() {
// No NWC — poll for confirmation (manual payment / QR code flow)
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 2000))
const checkRes = await fetch(`/api/payments/check/${paymentId}`)
const checkRes = await authFetch(`/api/payments/check/${paymentId}`)
if (checkRes.ok) {
const { status } = await checkRes.json()
if (status === 'confirmed') {
@@ -212,7 +213,7 @@ export function useWallet() {
}
async function submitCashuToken(botId: string, token: string): Promise<string> {
const res = await fetch('/api/payments/submit-cashu', {
const res = await authFetch('/api/payments/submit-cashu', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ botId, token }),