import { Event } from 'nostr-tools' import { nostrService } from './nostr' import { PLATFORM_NPUB } from './platformConfig' import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' import type { ContentDeliveryTracking } from './platformTrackingTypes' import { buildTrackingEvent, getTrackingKind } from './platformTrackingEvents' import { parseTrackingEvent } from './platformTrackingQueries' export type { ContentDeliveryTracking } from './platformTrackingTypes' /** * Platform tracking service * Publishes tracking events on Nostr for content delivery verification * These events are signed by the platform and can be queried for audit purposes */ export class PlatformTrackingService { private readonly platformPubkey: string = PLATFORM_NPUB private async publishTrackingEvent(event: Event): Promise { // Publish to all active relays via websocketService (routes to Service Worker) const { websocketService } = await import('./websocketService') const { relaySessionManager } = await import('./relaySessionManager') const activeRelays = await relaySessionManager.getActiveRelays() if (activeRelays.length === 0) { // Fallback to primary relay if no active relays const { getPrimaryRelaySync } = await import('./config') const relayUrl = getPrimaryRelaySync() await websocketService.publishEvent(event, [relayUrl]) } else { // Publish to all active relays console.warn(`[PlatformTracking] Publishing tracking event ${event.id} to ${activeRelays.length} active relay(s)`) await websocketService.publishEvent(event, activeRelays) } } private validateTrackingPool(): { pool: SimplePoolWithSub; authorPubkey: string } | null { const pool = nostrService.getPool() if (!pool) { console.error('Pool not initialized for platform tracking') return null } const authorPubkey = nostrService.getPublicKey() if (!authorPubkey) { console.error('Author public key not available for tracking') return null } return { pool, authorPubkey } } /** * Publish a content delivery tracking event * This event is published by the author but tagged for platform tracking * The platform can query these events to track all content deliveries */ async trackContentDelivery( tracking: ContentDeliveryTracking, authorPrivateKey: string ): Promise { try { const validation = this.validateTrackingPool() if (!validation) { return null } const { authorPubkey } = validation const event = buildTrackingEvent(tracking, authorPubkey, authorPrivateKey, this.platformPubkey) await this.publishTrackingEvent(event) console.warn('Platform tracking event published', { eventId: event.id, articleId: tracking.articleId, recipientPubkey: tracking.recipientPubkey, messageEventId: tracking.messageEventId, authorAmount: tracking.authorAmount, platformCommission: tracking.platformCommission, timestamp: new Date().toISOString(), }) return event.id } catch (error) { console.error('Error publishing platform tracking event', { articleId: tracking.articleId, recipientPubkey: tracking.recipientPubkey, error: error instanceof Error ? error.message : 'Unknown error', timestamp: new Date().toISOString(), }) return null } } /** * Query tracking events for an article * Returns all delivery tracking events for a specific article * Uses websocketService to route events to Service Worker */ async getArticleDeliveries(articleId: string): Promise { try { const { websocketService } = await import('./websocketService') const { getPrimaryRelaySync } = await import('./config') const { swClient } = await import('./swClient') const filters = [ { kinds: [getTrackingKind()], '#p': [this.platformPubkey], '#article': [articleId], limit: 100, }, ] const relayUrl = getPrimaryRelaySync() return new Promise((resolve) => { const deliveries: ContentDeliveryTracking[] = [] let resolved = false let unsubscribe: (() => void) | null = null let eoseReceived = false const finalize = (): void => { if (resolved) { return } resolved = true if (unsubscribe) { unsubscribe() } resolve(deliveries) } // Subscribe via websocketService (routes to Service Worker) void websocketService.subscribe([relayUrl], filters, (event: Event) => { const delivery = parseTrackingEvent(event) if (delivery) { deliveries.push(delivery) } }).then((unsub) => { unsubscribe = unsub }) // Listen for EOSE via Service Worker messages const handleEOSE = (data: unknown): void => { const eoseData = data as { relays: string[] } if (eoseData.relays.includes(relayUrl) && !eoseReceived) { eoseReceived = true finalize() } } swClient.onMessage('WEBSOCKET_EOSE', handleEOSE) setTimeout(() => { if (!eoseReceived) { finalize() } }, 5000) }) } catch (error) { console.error('Error querying article deliveries', { articleId, error: error instanceof Error ? error.message : 'Unknown error', }) return [] } } /** * Query all deliveries for a recipient * Uses websocketService to route events to Service Worker */ async getRecipientDeliveries(recipientPubkey: string): Promise { try { const { websocketService } = await import('./websocketService') const { getPrimaryRelaySync } = await import('./config') const { getTrackingKind } = await import('./platformTrackingEvents') const { swClient } = await import('./swClient') const filters = [ { kinds: [getTrackingKind()], '#p': [this.platformPubkey], '#recipient': [recipientPubkey], limit: 100, }, ] const relayUrl = getPrimaryRelaySync() return new Promise((resolve) => { const deliveries: ContentDeliveryTracking[] = [] let resolved = false let unsubscribe: (() => void) | null = null let eoseReceived = false const finalize = (): void => { if (resolved) { return } resolved = true if (unsubscribe) { unsubscribe() } resolve(deliveries) } // Subscribe via websocketService (routes to Service Worker) void websocketService.subscribe([relayUrl], filters, (event: Event) => { const delivery = parseTrackingEvent(event) if (delivery) { deliveries.push(delivery) } }).then((unsub) => { unsubscribe = unsub }) // Listen for EOSE via Service Worker messages const handleEOSE = (data: unknown): void => { const eoseData = data as { relays: string[] } if (eoseData.relays.includes(relayUrl) && !eoseReceived) { eoseReceived = true finalize() } } swClient.onMessage('WEBSOCKET_EOSE', handleEOSE) setTimeout(() => { if (!eoseReceived) { finalize() } }, 5000) }) } catch (error) { console.error('Error querying recipient deliveries', { recipientPubkey, error: error instanceof Error ? error.message : 'Unknown error', }) return [] } } } export const platformTracking = new PlatformTrackingService()