/** * NIP-98 HTTP Auth implementation * Generates authentication tokens for HTTP requests using Nostr events * See: https://github.com/nostr-protocol/nips/blob/master/98.md */ import type { EventTemplate } from 'nostr-tools' import { finalizeEvent } from 'nostr-tools' import { hexToBytes } from 'nostr-tools/utils' import { nostrService } from './nostr' import { nostrAuthService } from './nostrAuth' /** * Generate NIP-98 authentication token for HTTP request * @param method HTTP method (GET, POST, etc.) * @param url Full URL of the request * @param payloadHash Optional SHA256 hash of the request body (for POST/PUT) * @returns Base64-encoded signed event token */ export async function generateNip98Token(method: string, url: string, payloadHash?: string): Promise { const pubkey = getPubkeyOrThrow() const privateKey = getPrivateKeyOrThrow() const eventTemplate = buildNip98EventTemplate({ method, url, payloadHash, pubkey }) const signedEvent = finalizeEvent(eventTemplate, hexToBytes(privateKey)) return encodeEventAsBase64Json(signedEvent) } function getPubkeyOrThrow(): string { const pubkey = nostrService.getPublicKey() if (!pubkey) { throw new Error('Public key required for NIP-98 authentication. Please unlock your account.') } return pubkey } function getPrivateKeyOrThrow(): string { if (!nostrAuthService.isUnlocked()) { throw new Error( 'Private key required for NIP-98 authentication. Please unlock your account with your recovery phrase.' ) } const privateKey = nostrAuthService.getPrivateKey() if (!privateKey) { throw new Error('Private key not available. Please unlock your account.') } return privateKey } function buildNip98EventTemplate(params: { method: string url: string payloadHash: string | undefined pubkey: string }): EventTemplate & { pubkey: string } { const urlObj = new URL(params.url) const path = urlObj.pathname + urlObj.search const tags: string[][] = [ ['u', urlObj.origin + path], ['method', params.method], ] if (params.payloadHash) { tags.push(['payload', params.payloadHash]) } return { kind: 27235, created_at: Math.floor(Date.now() / 1000), tags, content: '', pubkey: params.pubkey, } } function encodeEventAsBase64Json(event: unknown): string { const eventJson = JSON.stringify(event) const eventBytes = new TextEncoder().encode(eventJson) return globalThis.btoa(String.fromCharCode(...eventBytes)) } /** * Check if NIP-98 authentication is available * Requires both public key and unlocked private key */ export function isNip98Available(): boolean { const pubkey = nostrService.getPublicKey() const isUnlocked = nostrAuthService.isUnlocked() return Boolean(pubkey) && isUnlocked }