chore(base): delete nostr-metadata-service + retire webapp-side kind-0 broadcast paths

Lnbits's cascade now publishes kind-0 user metadata server-side on
account creation AND on every PATCH /api/v1/auth (aiolabs/lnbits commit
869f67c3 folded into PR #26, deployed to aio-demo via server-deploy
e2eed9c). The webapp no longer needs its own kind-0 publish surface.

Changes:
- Delete src/modules/base/nostr/nostr-metadata-service.ts (162 lines).
  Server now owns kind-0 lifecycle via NostrSigner.sign_event.
- Delete src/modules/base/composables/useNostrMetadata.ts (had zero
  callers; was just a thin wrapper around the deleted service).
- Remove NOSTR_METADATA_SERVICE token from di-container.ts.
- Remove all NostrMetadataService imports / instantiations /
  registrations / dispose calls from src/modules/base/index.ts.
- src/modules/base/auth/auth-service.ts:
  - Drop the broadcastNostrMetadata() helper entirely.
  - Drop its callers in login() (was line 118 pre-edit) and register()
    (was line 142 pre-edit) — both flagged for removal by lnbits in the
    01:45Z coordination handoff. Login-time republish was always
    redundant for kind-0 (replaceable event); register-time is covered
    by lnbits's create_user_account -> _publish_nostr_metadata_event
    path.
  - Drop the auto-broadcast in updateProfile() too — covered by the
    PATCH /api/v1/auth handler's _publish_nostr_metadata_event call
    per the gap-fill commit.
  - Leave the prvkey/pubkey preservation in updateProfile() in place
    for now; the prvkey field removal is the atomic phase-1 final PR
    per design doc Q1.2 Option (b).
- src/modules/base/components/ProfileSettings.vue:
  - Remove the "Broadcast to Nostr" button + isBroadcasting state +
    Radio icon + broadcastMetadata() handler. Manual re-broadcast was
    a local-testing safety net for relay resets that's no longer
    needed once the server publishes automatically on profile save.
  - Simplify the post-save toast to a generic "Profile updated!".
  - Update the helper text accordingly.

This is webapp's bucket-A leg per aiolabs/lnbits#9 phase-1 plan.

Refs:
- log:2026-05-29T01:45Z (lnbits handoff identifying the auth-service
  line numbers to drop)
- ~/dev/coordination/webapp-design-questions.md Q2.3 (decision context)
- aiolabs/lnbits PR #26 commit 869f67c3 (server-side kind-0 publish)
- aiolabs/lnbits dev tip 861f427c, deployed to aio-demo

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-29 21:28:48 +02:00
commit 414b79565c
6 changed files with 17 additions and 300 deletions

View file

@ -144,9 +144,6 @@ export const SERVICE_TOKENS = {
SUBMISSION_SERVICE: Symbol('submissionService'), SUBMISSION_SERVICE: Symbol('submissionService'),
LINK_PREVIEW_SERVICE: Symbol('linkPreviewService'), LINK_PREVIEW_SERVICE: Symbol('linkPreviewService'),
// Nostr metadata services
NOSTR_METADATA_SERVICE: Symbol('nostrMetadataService'),
// Nostr transport (kind-21000 RPC over relays — LNbits backend) // Nostr transport (kind-21000 RPC over relays — LNbits backend)
NOSTR_TRANSPORT_SERVICE: Symbol('nostrTransportService'), NOSTR_TRANSPORT_SERVICE: Symbol('nostrTransportService'),

View file

@ -2,9 +2,7 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { BaseService } from '@/core/base/BaseService' import { BaseService } from '@/core/base/BaseService'
import { eventBus } from '@/core/event-bus' import { eventBus } from '@/core/event-bus'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import type { LoginCredentials, RegisterData, User } from '@/lib/api/lnbits' import type { LoginCredentials, RegisterData, User } from '@/lib/api/lnbits'
import type { NostrMetadataService } from '../nostr/nostr-metadata-service'
import { getPendingAuthToken, removePendingAuthToken } from '@/lib/config/lnbits' import { getPendingAuthToken, removePendingAuthToken } from '@/lib/config/lnbits'
export class AuthService extends BaseService { export class AuthService extends BaseService {
@ -114,9 +112,6 @@ export class AuthService extends BaseService {
eventBus.emit('auth:login', { user: userData }, 'auth-service') eventBus.emit('auth:login', { user: userData }, 'auth-service')
// Auto-broadcast Nostr metadata on login
this.broadcastNostrMetadata()
} catch (error) { } catch (error) {
const err = this.handleError(error, 'login') const err = this.handleError(error, 'login')
eventBus.emit('auth:login-failed', { error: err }, 'auth-service') eventBus.emit('auth:login-failed', { error: err }, 'auth-service')
@ -138,9 +133,6 @@ export class AuthService extends BaseService {
eventBus.emit('auth:login', { user: userData }, 'auth-service') eventBus.emit('auth:login', { user: userData }, 'auth-service')
// Auto-broadcast Nostr metadata on registration
this.broadcastNostrMetadata()
} catch (error) { } catch (error) {
const err = this.handleError(error, 'register') const err = this.handleError(error, 'register')
eventBus.emit('auth:login-failed', { error: err }, 'auth-service') eventBus.emit('auth:login-failed', { error: err }, 'auth-service')
@ -195,10 +187,9 @@ export class AuthService extends BaseService {
prvkey: this.user.value?.prvkey || updatedUser.prvkey prvkey: this.user.value?.prvkey || updatedUser.prvkey
} }
// Auto-broadcast Nostr metadata when profile is updated // Kind-0 metadata is published server-side by lnbits's PATCH /auth handler
// Note: ProfileSettings component will also manually broadcast, // (aiolabs/lnbits commit 869f67c3) once the cascade is deployed. The webapp
// but this ensures metadata stays in sync even if updated elsewhere // no longer maintains its own broadcast path.
this.broadcastNostrMetadata()
} catch (error) { } catch (error) {
const err = this.handleError(error, 'updateProfile') const err = this.handleError(error, 'updateProfile')
@ -208,26 +199,6 @@ export class AuthService extends BaseService {
} }
} }
/**
* Broadcast user metadata to Nostr relays (NIP-01 kind 0)
* Called automatically on login, registration, and profile updates
*/
private async broadcastNostrMetadata(): Promise<void> {
try {
const metadataService = injectService<NostrMetadataService>(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
if (metadataService && this.user.value?.pubkey) {
// Broadcast in background - don't block login/update
metadataService.publishMetadata().catch(error => {
console.warn('Failed to broadcast Nostr metadata:', error)
// Don't throw - this is a non-critical background operation
})
}
} catch (error) {
// If service isn't available yet, silently skip
console.debug('Nostr metadata service not yet available')
}
}
/** /**
* Cleanup when service is disposed * Cleanup when service is disposed
*/ */

View file

@ -122,32 +122,17 @@
</div> </div>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="flex flex-col sm:flex-row gap-3"> <Button
<Button type="submit"
type="submit" :disabled="isUpdating || !isFormValid"
:disabled="isUpdating || !isFormValid" class="w-full"
class="flex-1" >
> <span v-if="isUpdating">Updating...</span>
<span v-if="isUpdating">Updating...</span> <span v-else>Update Profile</span>
<span v-else>Update Profile</span> </Button>
</Button>
<Button
type="button"
variant="outline"
:disabled="isBroadcasting"
@click="broadcastMetadata"
class="flex-1"
>
<Radio class="mr-2 h-4 w-4" :class="{ 'animate-pulse': isBroadcasting }" />
<span v-if="isBroadcasting">Broadcasting...</span>
<span v-else>Broadcast to Nostr</span>
</Button>
</div>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
Your profile is automatically broadcast to Nostr when you update it or log in. Your profile is broadcast to Nostr automatically when you save changes.
Use the "Broadcast to Nostr" button to manually re-broadcast your profile.
</p> </p>
</form> </form>
@ -189,7 +174,7 @@ import * as z from 'zod'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { User, Zap, Hash, Radio } from 'lucide-vue-next' import { User, Zap, Hash } from 'lucide-vue-next'
import { import {
FormControl, FormControl,
FormDescription, FormDescription,
@ -215,19 +200,16 @@ import { useAuth } from '@/composables/useAuthService'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { injectService, SERVICE_TOKENS } from '@/core/di-container' import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import type { ImageUploadService } from '../services/ImageUploadService' import type { ImageUploadService } from '../services/ImageUploadService'
import type { NostrMetadataService } from '../nostr/nostr-metadata-service'
import { useToast } from '@/core/composables/useToast' import { useToast } from '@/core/composables/useToast'
// Services // Services
const { user, updateProfile, logout } = useAuth() const { user, updateProfile, logout } = useAuth()
const router = useRouter() const router = useRouter()
const imageService = injectService<ImageUploadService>(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE) const imageService = injectService<ImageUploadService>(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE)
const metadataService = injectService<NostrMetadataService>(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
const toast = useToast() const toast = useToast()
// Local state // Local state
const isUpdating = ref(false) const isUpdating = ref(false)
const isBroadcasting = ref(false)
const updateError = ref<string | null>(null) const updateError = ref<string | null>(null)
const updateSuccess = ref(false) const updateSuccess = ref(false)
const uploadedPicture = ref<any[]>([]) const uploadedPicture = ref<any[]>([])
@ -323,18 +305,12 @@ const updateUserProfile = async (formData: any) => {
} }
} }
// Update profile via AuthService (which updates LNbits) // Update profile via AuthService (which updates LNbits).
// Kind-0 metadata publishing happens server-side as part of the
// PATCH /api/v1/auth handler (aiolabs/lnbits 869f67c3).
await updateProfile(updateData) await updateProfile(updateData)
// Broadcast to Nostr automatically toast.success('Profile updated!')
try {
await metadataService.publishMetadata()
toast.success('Profile updated and broadcast to Nostr!')
} catch (nostrError) {
console.error('Failed to broadcast to Nostr:', nostrError)
toast.warning('Profile updated, but failed to broadcast to Nostr')
}
updateSuccess.value = true updateSuccess.value = true
// Clear success message after 3 seconds // Clear success message after 3 seconds
@ -352,22 +328,6 @@ const updateUserProfile = async (formData: any) => {
} }
} }
// Manually broadcast metadata to Nostr
const broadcastMetadata = async () => {
isBroadcasting.value = true
try {
const result = await metadataService.publishMetadata()
toast.success(`Profile broadcast to ${result.success}/${result.total} relays!`)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to broadcast metadata'
console.error('Error broadcasting metadata:', error)
toast.error(`Failed to broadcast: ${errorMessage}`)
} finally {
isBroadcasting.value = false
}
}
// Log out + redirect to /login on this app's origin. // Log out + redirect to /login on this app's origin.
const onLogout = async () => { const onLogout = async () => {
try { try {

View file

@ -1,39 +0,0 @@
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import type { NostrMetadataService, NostrMetadata } from '../nostr/nostr-metadata-service'
/**
* Composable for accessing Nostr metadata service
*
* @example
* ```ts
* const { publishMetadata, getMetadata } = useNostrMetadata()
*
* // Get current metadata
* const metadata = getMetadata()
*
* // Publish metadata to Nostr relays
* await publishMetadata()
* ```
*/
export function useNostrMetadata() {
const metadataService = injectService<NostrMetadataService>(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
/**
* Publish user metadata to Nostr relays (NIP-01 kind 0)
*/
const publishMetadata = async (): Promise<{ success: number; total: number }> => {
return await metadataService.publishMetadata()
}
/**
* Get current user's Nostr metadata
*/
const getMetadata = (): NostrMetadata => {
return metadataService.getMetadata()
}
return {
publishMetadata,
getMetadata
}
}

View file

@ -2,7 +2,6 @@ import type { App } from 'vue'
import type { ModulePlugin } from '@/core/types' import type { ModulePlugin } from '@/core/types'
import { container, SERVICE_TOKENS } from '@/core/di-container' import { container, SERVICE_TOKENS } from '@/core/di-container'
import { relayHub } from './nostr/relay-hub' import { relayHub } from './nostr/relay-hub'
import { NostrMetadataService } from './nostr/nostr-metadata-service'
import { ProfileService } from './nostr/ProfileService' import { ProfileService } from './nostr/ProfileService'
import { ReactionService } from './nostr/ReactionService' import { ReactionService } from './nostr/ReactionService'
import { NostrTransportService } from './services/NostrTransportService' import { NostrTransportService } from './services/NostrTransportService'
@ -30,7 +29,6 @@ import ProfileSettings from './components/ProfileSettings.vue'
const invoiceService = new InvoiceService() const invoiceService = new InvoiceService()
const lnbitsAPI = new LnbitsAPI() const lnbitsAPI = new LnbitsAPI()
const imageUploadService = new ImageUploadService() const imageUploadService = new ImageUploadService()
const nostrMetadataService = new NostrMetadataService()
const profileService = new ProfileService() const profileService = new ProfileService()
const reactionService = new ReactionService() const reactionService = new ReactionService()
const nostrTransportService = new NostrTransportService() const nostrTransportService = new NostrTransportService()
@ -48,7 +46,6 @@ export const baseModule: ModulePlugin = {
// Register core Nostr services // Register core Nostr services
container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub) container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
container.provide(SERVICE_TOKENS.NOSTR_METADATA_SERVICE, nostrMetadataService)
// Register auth service // Register auth service
container.provide(SERVICE_TOKENS.AUTH_SERVICE, auth) container.provide(SERVICE_TOKENS.AUTH_SERVICE, auth)
@ -113,10 +110,6 @@ export const baseModule: ModulePlugin = {
waitForDependencies: true, // ImageUploadService depends on ToastService waitForDependencies: true, // ImageUploadService depends on ToastService
maxRetries: 3 maxRetries: 3
}) })
await nostrMetadataService.initialize({
waitForDependencies: true, // NostrMetadataService depends on AuthService and RelayHub
maxRetries: 3
})
await profileService.initialize({ await profileService.initialize({
waitForDependencies: true, // ProfileService depends on RelayHub waitForDependencies: true, // ProfileService depends on RelayHub
maxRetries: 3 maxRetries: 3
@ -145,7 +138,6 @@ export const baseModule: ModulePlugin = {
await storageService.dispose() await storageService.dispose()
await toastService.dispose() await toastService.dispose()
await imageUploadService.dispose() await imageUploadService.dispose()
await nostrMetadataService.dispose()
await profileService.dispose() await profileService.dispose()
await reactionService.dispose() await reactionService.dispose()
await nostrTransportService.dispose() await nostrTransportService.dispose()
@ -156,7 +148,6 @@ export const baseModule: ModulePlugin = {
container.remove(SERVICE_TOKENS.LNBITS_API) container.remove(SERVICE_TOKENS.LNBITS_API)
container.remove(SERVICE_TOKENS.INVOICE_SERVICE) container.remove(SERVICE_TOKENS.INVOICE_SERVICE)
container.remove(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE) container.remove(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE)
container.remove(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
container.remove(SERVICE_TOKENS.PROFILE_SERVICE) container.remove(SERVICE_TOKENS.PROFILE_SERVICE)
container.remove(SERVICE_TOKENS.REACTION_SERVICE) container.remove(SERVICE_TOKENS.REACTION_SERVICE)
@ -173,7 +164,6 @@ export const baseModule: ModulePlugin = {
invoiceService, invoiceService,
pwaService, pwaService,
imageUploadService, imageUploadService,
nostrMetadataService,
profileService, profileService,
reactionService reactionService
}, },

View file

@ -1,162 +0,0 @@
import { BaseService } from '@/core/base/BaseService'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import { finalizeEvent, type EventTemplate } from 'nostr-tools'
import type { AuthService } from '@/modules/base/auth/auth-service'
import type { RelayHub } from '@/modules/base/nostr/relay-hub'
/**
* Nostr User Metadata (NIP-01 kind 0)
* https://github.com/nostr-protocol/nips/blob/master/01.md
*/
export interface NostrMetadata {
name?: string // Display name (from username)
display_name?: string // Alternative display name
about?: string // Bio/description
picture?: string // Profile picture URL
banner?: string // Profile banner URL
nip05?: string // NIP-05 identifier (username@domain)
lud16?: string // Lightning Address (same as nip05)
website?: string // Personal website
}
/**
* Service for publishing and managing Nostr user metadata (NIP-01 kind 0)
*
* This service handles:
* - Publishing user profile metadata to Nostr relays
* - Syncing LNbits user data with Nostr profile
* - Auto-broadcasting metadata on login and profile updates
*/
export class NostrMetadataService extends BaseService {
protected readonly metadata = {
name: 'NostrMetadataService',
version: '1.0.0',
dependencies: ['AuthService', 'RelayHub']
}
protected authService: AuthService | null = null
protected relayHub: RelayHub | null = null
protected async onInitialize(): Promise<void> {
console.log('NostrMetadataService: Starting initialization...')
this.authService = injectService<AuthService>(SERVICE_TOKENS.AUTH_SERVICE)
this.relayHub = injectService<RelayHub>(SERVICE_TOKENS.RELAY_HUB)
if (!this.authService) {
throw new Error('AuthService not available')
}
if (!this.relayHub) {
throw new Error('RelayHub service not available')
}
console.log('NostrMetadataService: Initialization complete')
}
/**
* Build Nostr metadata from LNbits user data
*/
private buildMetadata(): NostrMetadata {
const user = this.authService?.user.value
if (!user) {
throw new Error('No authenticated user')
}
const lightningDomain = import.meta.env.VITE_LIGHTNING_DOMAIN || window.location.hostname
const username = user.username || user.id.slice(0, 8)
const metadata: NostrMetadata = {
name: username,
nip05: `${username}@${lightningDomain}`,
lud16: `${username}@${lightningDomain}`
}
// Add optional fields from user.extra if they exist
if (user.extra?.display_name) {
metadata.display_name = user.extra.display_name
}
if (user.extra?.picture) {
metadata.picture = user.extra.picture
}
return metadata
}
/**
* Publish user metadata to Nostr relays (NIP-01 kind 0)
*
* This creates a replaceable event that updates the user's profile.
* Only the latest kind 0 event for a given pubkey is kept by relays.
*/
async publishMetadata(): Promise<{ success: number; total: number }> {
if (!this.authService?.isAuthenticated.value) {
throw new Error('Must be authenticated to publish metadata')
}
if (!this.relayHub?.isConnected.value) {
throw new Error('Not connected to relays')
}
const user = this.authService.user.value
if (!user?.prvkey) {
throw new Error('User private key not available')
}
try {
const metadata = this.buildMetadata()
console.log('📤 Publishing Nostr metadata (kind 0):', metadata)
// Create kind 0 event (user metadata)
// Content is JSON-stringified metadata
const eventTemplate: EventTemplate = {
kind: 0,
content: JSON.stringify(metadata),
tags: [],
created_at: Math.floor(Date.now() / 1000)
}
// Sign the event
const privkeyBytes = this.hexToUint8Array(user.prvkey)
const signedEvent = finalizeEvent(eventTemplate, privkeyBytes)
console.log('✅ Metadata event signed:', signedEvent.id)
console.log('📋 Full signed event:', JSON.stringify(signedEvent, null, 2))
// Publish to all connected relays
const result = await this.relayHub.publishEvent(signedEvent)
console.log(`✅ Metadata published to ${result.success}/${result.total} relays`)
return result
} catch (error) {
console.error('Failed to publish metadata:', error)
throw error
}
}
/**
* Get current user's Nostr metadata
*/
getMetadata(): NostrMetadata {
return this.buildMetadata()
}
/**
* Helper function to convert hex string to Uint8Array
*/
private hexToUint8Array(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2)
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
}
return bytes
}
protected async onDestroy(): Promise<void> {
// Cleanup if needed
}
}