diff --git a/src/modules/market/composables/useMarket.ts b/src/modules/market/composables/useMarket.ts index 62c66da..66af02c 100644 --- a/src/modules/market/composables/useMarket.ts +++ b/src/modules/market/composables/useMarket.ts @@ -3,7 +3,7 @@ import { useMarketStore } from '../stores/market' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import { config } from '@/lib/config' import type { NostrmarketService } from '../services/nostrmarketService' -import { nip59 } from 'nostr-tools' +import { nip04 } from 'nostr-tools' import { useAsyncOperation } from '@/core/composables/useAsyncOperation' import { auth } from '@/composables/useAuthService' @@ -54,29 +54,19 @@ export function useMarket() { throw new Error('AuthService not available. Make sure base module is installed.') } - // Subscribe to incoming order gift wraps (NIP-17 / kind 1059) addressed to the user. - // - // The chat service still runs on NIP-04 (kind 4); when it migrates to NIP-17 it - // can take over routing of order DMs the way it does today via setMarketMessageHandler. - // Until then the market subscribes directly so order flows aren't dependent on chat. + // Register market DM handler with chat service (if available) const registerMarketMessageHandler = () => { try { - const userPubkey = authService.user.value?.pubkey || auth.currentUser.value?.pubkey - if (!userPubkey) { - console.log('🛒 No user pubkey available; skipping order gift-wrap subscription') - return + // Try to get the chat service (it might not be available if chat module isn't loaded) + const chatService = (globalThis as any).chatService + if (chatService && chatService.setMarketMessageHandler) { + chatService.setMarketMessageHandler(handleOrderDM) + console.log('🛒 Registered market message handler with chat service') + } else { + console.log('🛒 Chat service not available, market will use its own DM subscription') } - - const unsubscribe = relayHub.subscribe({ - id: `market-orders-${userPubkey.slice(0, 16)}`, - filters: [{ kinds: [1059], '#p': [userPubkey] }], - onEvent: (event: any) => handleOrderDM(event) - }) - console.log('🎁 Subscribed to order gift wraps (kind 1059)') - // unsubscribe is currently not retained; market lifecycle owns this - void unsubscribe } catch (error) { - console.warn('🛒 Failed to subscribe to order gift wraps:', error) + console.log('🛒 Could not register with chat service:', error) } } @@ -433,46 +423,53 @@ export function useMarket() { return null } - // Convert hex string to Uint8Array (browser-compatible) - const 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.slice(i, i + 2), 16) - } - return bytes - } - - // Handle incoming order gift wraps (kind 1059) — payment requests, status updates. - // - // The outer event's pubkey is an ephemeral key (NIP-59); the real merchant - // pubkey is on the unwrapped rumor. Content is JSON with a `type` field - // (1 = payment request, 2 = order status update). + // Handle incoming order DMs (payment requests, status updates) const handleOrderDM = async (event: any) => { try { - console.log('🎁 Received order gift wrap:', event.id, '(kind', event.kind + ')') - - const userPrivkey = - authService.user.value?.prvkey ?? auth.currentUser.value?.prvkey - - if (!userPrivkey) { - console.warn('Cannot unwrap gift wrap: no user private key available') + console.log('🔔 Received order-related DM:', event.id, 'from:', event.pubkey.slice(0, 8)) + + // Check both injected auth service AND global auth composable + const hasAuthService = authService.user.value?.prvkey + const hasGlobalAuth = auth.currentUser.value?.prvkey + + const userPrivkey = hasAuthService ? authService.user.value.prvkey : auth.currentUser.value?.prvkey + const userPubkey = hasAuthService ? authService.user.value.pubkey : auth.currentUser.value?.pubkey + + if (!userPrivkey || !userPubkey) { + console.warn('Cannot decrypt DM: no user private key available', { + hasAuthService: !!hasAuthService, + hasGlobalAuth: !!hasGlobalAuth, + authServicePrivkey: !!authService.user.value?.prvkey, + globalAuthPrivkey: !!auth.currentUser.value?.prvkey + }) return } + + console.log('🔐 Market DM decryption auth check:', { + hasAuthService: !!hasAuthService, + hasGlobalAuth: !!hasGlobalAuth, + usingAuthService: !!hasAuthService, + userPubkey: userPubkey.substring(0, 10) + '...' + }) - const prvkeyBytes = hexToUint8Array(userPrivkey) - const rumor = nip59.unwrapEvent(event, prvkeyBytes) - console.log('🔓 Unwrapped rumor from merchant:', rumor.pubkey.slice(0, 10) + '...') + console.log('🔓 Attempting to decrypt DM with private key available') - const messageData = JSON.parse(rumor.content) + // Decrypt the DM content + const decryptedContent = await nip04.decrypt(userPrivkey, event.pubkey, event.content) + console.log('🔓 Decrypted DM content:', decryptedContent) + + // Parse the decrypted content as JSON + const messageData = JSON.parse(decryptedContent) console.log('📨 Parsed message data:', messageData) + // Handle different types of messages switch (messageData.type) { case 1: // Payment request console.log('💰 Processing payment request for order:', messageData.id) await nostrmarketService.handlePaymentRequest(messageData) console.log('✅ Payment request processed successfully') break - case 2: // Order status update + case 2: // Order status update console.log('📦 Processing order status update for order:', messageData.id) await nostrmarketService.handleOrderStatusUpdate(messageData) console.log('✅ Order status update processed successfully') @@ -481,7 +478,7 @@ export function useMarket() { console.log('❓ Unknown message type:', messageData.type) } } catch (error) { - console.error('Failed to handle order gift wrap:', error) + console.error('Failed to handle order DM:', error) } } diff --git a/src/modules/market/services/nostrmarketService.ts b/src/modules/market/services/nostrmarketService.ts index 6598d38..733879e 100644 --- a/src/modules/market/services/nostrmarketService.ts +++ b/src/modules/market/services/nostrmarketService.ts @@ -1,4 +1,4 @@ -import { type EventTemplate, nip59 } from 'nostr-tools' +import { finalizeEvent, type EventTemplate, nip04 } from 'nostr-tools' import { BaseService } from '@/core/base/BaseService' import type { Order } from '@/modules/market/stores/market' @@ -159,17 +159,12 @@ export class NostrmarketService extends BaseService { // Stall and product publishing is now handled by LNbits API endpoints /** - * Publish an order as a NIP-59 gift-wrapped (kind 1059) event to nostrmarket. - * - * The order JSON is placed in an unsigned kind 14 rumor, sealed (kind 13) - * with the customer's key, and wrapped (kind 1059) with an ephemeral key. - * Only the merchant can decrypt the wrap; the public event reveals nothing - * about the sender. + * Publish an order event (kind 4 encrypted DM) to nostrmarket */ async publishOrder(order: Order, merchantPubkey: string): Promise { const { prvkey } = this.getAuth() - - // Convert order to nostrmarket format - matches NIP-15 customer order spec + + // Convert order to nostrmarket format - exactly matching the specification const orderData = { type: 0, // DirectMessageType.CUSTOMER_ORDER id: order.id, @@ -180,37 +175,72 @@ export class NostrmarketService extends BaseService { contact: { name: order.contactInfo?.message || order.contactInfo?.email || 'Unknown', email: order.contactInfo?.email || '' + // Remove phone field - not in nostrmarket specification }, + // Only include address if it's a physical good and address is provided ...(order.shippingZone?.requiresPhysicalShipping && order.contactInfo?.address ? { address: order.contactInfo.address } : {}), shipping_id: order.shippingZone?.id || 'online' } - const rumorTemplate: Partial = { - kind: 14, - tags: [['p', merchantPubkey]], - content: JSON.stringify(orderData), + // Encrypt the message using NIP-04 + console.log('🔐 NIP-04 encryption debug:', { + prvkeyType: typeof prvkey, + prvkeyIsString: typeof prvkey === 'string', + prvkeyLength: prvkey.length, + prvkeySample: prvkey.substring(0, 10) + '...', + merchantPubkeyType: typeof merchantPubkey, + merchantPubkeyLength: merchantPubkey.length, + orderDataString: JSON.stringify(orderData).substring(0, 50) + '...' + }) + + let encryptedContent: string + try { + encryptedContent = await nip04.encrypt(prvkey, merchantPubkey, JSON.stringify(orderData)) + console.log('🔐 NIP-04 encryption successful:', { + encryptedContentLength: encryptedContent.length, + encryptedContentSample: encryptedContent.substring(0, 50) + '...' + }) + } catch (error) { + console.error('🔐 NIP-04 encryption failed:', error) + throw error + } + + const eventTemplate: EventTemplate = { + kind: 4, // Encrypted DM + tags: [['p', merchantPubkey]], // Recipient (merchant) + content: encryptedContent, // Use encrypted content created_at: Math.floor(Date.now() / 1000) } - const prvkeyBytes = this.hexToUint8Array(prvkey) - const giftWrap = nip59.wrapEvent(rumorTemplate, prvkeyBytes, merchantPubkey) - - console.log('🎁 Order gift-wrapped (NIP-17):', { - orderId: order.id, - giftWrapId: giftWrap.id, - kind: giftWrap.kind, - merchantPubkey: merchantPubkey.substring(0, 10) + '...' + console.log('🔧 finalizeEvent debug:', { + prvkeyType: typeof prvkey, + prvkeyIsString: typeof prvkey === 'string', + prvkeyLength: prvkey.length, + prvkeySample: prvkey.substring(0, 10) + '...', + encodedPrvkeyType: typeof new TextEncoder().encode(prvkey), + encodedPrvkeyLength: new TextEncoder().encode(prvkey).length, + eventTemplate }) - const result = await this.relayHub.publishEvent(giftWrap) + // Convert hex string to Uint8Array properly + const prvkeyBytes = this.hexToUint8Array(prvkey) + console.log('🔧 prvkeyBytes debug:', { + prvkeyBytesType: typeof prvkeyBytes, + prvkeyBytesLength: prvkeyBytes.length, + prvkeyBytesIsUint8Array: prvkeyBytes instanceof Uint8Array + }) + const event = finalizeEvent(eventTemplate, prvkeyBytes) + const result = await this.relayHub.publishEvent(event) + console.log('Order published to nostrmarket:', { orderId: order.id, - eventId: giftWrap.id, + eventId: result, merchantPubkey, - content: orderData + content: orderData, + encryptedContent: encryptedContent.substring(0, 50) + '...' }) return result.success.toString() diff --git a/vite.activities.config.ts b/vite.activities.config.ts index b7627e4..aea9915 100644 --- a/vite.activities.config.ts +++ b/vite.activities.config.ts @@ -15,16 +15,13 @@ function activitiesHtmlPlugin(): Plugin { name: 'activities-html-rewrite', configureServer(server) { server.middlewares.use((req, _res, next) => { - // Rewrite all non-asset requests to activities.html. - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' + // Rewrite all non-asset requests to activities.html if ( req.url && !req.url.startsWith('/@') && !req.url.startsWith('/src/') && !req.url.startsWith('/node_modules/') && - !path.includes('.') + !req.url.includes('.') // skip files with extensions ) { req.url = '/activities.html' } diff --git a/vite.castle.config.ts b/vite.castle.config.ts index 6ec7e0d..d0916eb 100644 --- a/vite.castle.config.ts +++ b/vite.castle.config.ts @@ -15,16 +15,13 @@ function castleHtmlPlugin(): Plugin { name: 'castle-html-rewrite', configureServer(server) { server.middlewares.use((req, _res, next) => { - // Rewrite all non-asset requests to castle.html. - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' + // Rewrite all non-asset requests to castle.html if ( req.url && !req.url.startsWith('/@') && !req.url.startsWith('/src/') && !req.url.startsWith('/node_modules/') && - !path.includes('.') + !req.url.includes('.') // skip files with extensions ) { req.url = '/castle.html' } diff --git a/vite.chat.config.ts b/vite.chat.config.ts index c28535f..6965d08 100644 --- a/vite.chat.config.ts +++ b/vite.chat.config.ts @@ -11,15 +11,12 @@ function chatHtmlPlugin(): Plugin { name: 'chat-html-rewrite', configureServer(server) { server.middlewares.use((req, _res, next) => { - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' if ( req.url && !req.url.startsWith('/@') && !req.url.startsWith('/src/') && !req.url.startsWith('/node_modules/') && - !path.includes('.') + !req.url.includes('.') ) { req.url = '/chat.html' } diff --git a/vite.forum.config.ts b/vite.forum.config.ts index 0fdebbe..756d5c1 100644 --- a/vite.forum.config.ts +++ b/vite.forum.config.ts @@ -11,15 +11,12 @@ function forumHtmlPlugin(): Plugin { name: 'forum-html-rewrite', configureServer(server) { server.middlewares.use((req, _res, next) => { - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' if ( req.url && !req.url.startsWith('/@') && !req.url.startsWith('/src/') && !req.url.startsWith('/node_modules/') && - !path.includes('.') + !req.url.includes('.') ) { req.url = '/forum.html' } diff --git a/vite.market.config.ts b/vite.market.config.ts index bf38430..255d8c0 100644 --- a/vite.market.config.ts +++ b/vite.market.config.ts @@ -11,15 +11,12 @@ function marketHtmlPlugin(): Plugin { name: 'market-html-rewrite', configureServer(server) { server.middlewares.use((req, _res, next) => { - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' if ( req.url && !req.url.startsWith('/@') && !req.url.startsWith('/src/') && !req.url.startsWith('/node_modules/') && - !path.includes('.') + !req.url.includes('.') ) { req.url = '/market.html' } diff --git a/vite.tasks.config.ts b/vite.tasks.config.ts index 1edc3e6..3cb15fd 100644 --- a/vite.tasks.config.ts +++ b/vite.tasks.config.ts @@ -11,15 +11,12 @@ function tasksHtmlPlugin(): Plugin { name: 'tasks-html-rewrite', configureServer(server) { server.middlewares.use((req, _res, next) => { - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' if ( req.url && !req.url.startsWith('/@') && !req.url.startsWith('/src/') && !req.url.startsWith('/node_modules/') && - !path.includes('.') + !req.url.includes('.') ) { req.url = '/tasks.html' } diff --git a/vite.wallet.config.ts b/vite.wallet.config.ts index f991672..dfa0bd6 100644 --- a/vite.wallet.config.ts +++ b/vite.wallet.config.ts @@ -15,15 +15,12 @@ function walletHtmlPlugin(): Plugin { name: 'wallet-html-rewrite', configureServer(server) { server.middlewares.use((req, _res, next) => { - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' if ( req.url && !req.url.startsWith('/@') && !req.url.startsWith('/src/') && !req.url.startsWith('/node_modules/') && - !path.includes('.') + !req.url.includes('.') ) { req.url = '/wallet.html' }