/** * TimeSafari Integration Service * * Implements privacy-preserving claims architecture integration with endorser.ch, * DIDs (Decentralized Identifiers), and cryptographic verification patterns. * * @author Matthew Raymer * @version 1.0.0 */ import { OffersResponse, OffersToPlansResponse, PlansLastUpdatedResponse, TimeSafariNotificationBundle, TimeSafariUserConfig } from './definitions'; /** * TimeSafari Integration Service * * Handles integration with TimeSafari's privacy-preserving claims architecture * including DIDs, cryptographic verification, and endorser.ch API integration. */ export class TimeSafariIntegrationService { private static instance: TimeSafariIntegrationService; private activeDid: string | null = null; private veramoStack: unknown = null; private endorserApiBaseUrl: string; private constructor() { this.endorserApiBaseUrl = 'https://endorser.ch/api/v1'; } /** * Get singleton instance */ static getInstance(): TimeSafariIntegrationService { if (!TimeSafariIntegrationService.instance) { TimeSafariIntegrationService.instance = new TimeSafariIntegrationService(); } return TimeSafariIntegrationService.instance; } /** * Initialize TimeSafari integration * * @param config - TimeSafari integration configuration */ async initialize(config: TimeSafariIntegrationConfig): Promise { try { // Initializing TimeSafari integration service // Set active DID this.activeDid = config.activeDid; // Initialize Veramo stack if available if (config.veramoStack) { this.veramoStack = config.veramoStack; // Veramo stack initialized successfully } // Storage adapter is handled by the caller // Set API base URL if provided if (config.endorserApiBaseUrl) { this.endorserApiBaseUrl = config.endorserApiBaseUrl; } // TimeSafari integration initialization complete } catch (error) { throw new Error(`TimeSafari integration initialization failed: ${error}`); } } /** * Fetch TimeSafari community data with privacy-preserving patterns * * @param config - User configuration for data fetching * @returns TimeSafari notification bundle */ async fetchCommunityData(config: TimeSafariUserConfig): Promise { try { // Fetching TimeSafari community data const bundle: TimeSafariNotificationBundle = { fetchTimestamp: Date.now(), success: false, metadata: { activeDid: config.activeDid, fetchDurationMs: 0, cachedResponses: 0, networkResponses: 0 } }; const startTime = Date.now(); // Fetch offers to person if enabled if (config.fetchOffersToPerson) { try { bundle.offersToPerson = await this.fetchOffersToPerson(config.activeDid); if (bundle.metadata) bundle.metadata.networkResponses++; } catch (error) { // Failed to fetch offers to person, continuing with other data } } // Fetch offers to projects if enabled if (config.fetchOffersToProjects) { try { bundle.offersToProjects = await this.fetchOffersToProjects(config.activeDid); if (bundle.metadata) bundle.metadata.networkResponses++; } catch (error) { // Failed to fetch offers to projects, continuing with other data } } // Fetch project updates if enabled if (config.fetchProjectUpdates) { try { bundle.projectUpdates = await this.fetchProjectUpdates(config.activeDid, config.starredPlanIds); if (bundle.metadata) bundle.metadata.networkResponses++; } catch (error) { // Failed to fetch project updates, continuing with other data } } if (bundle.metadata) bundle.metadata.fetchDurationMs = Date.now() - startTime; bundle.success = true; // Community data fetch complete return bundle; } catch (error) { throw new Error(`Community data fetch failed: ${error}`); } } /** * Fetch offers to person with DID-based authentication * * @param activeDid - Active DID for authentication * @returns Offers response */ private async fetchOffersToPerson(activeDid: string): Promise { const url = `${this.endorserApiBaseUrl}/offers-to-person`; const headers = await this.getAuthenticatedHeaders(activeDid); const response = await fetch(url, { headers }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); } /** * Fetch offers to projects with DID-based authentication * * @param activeDid - Active DID for authentication * @returns Offers to plans response */ private async fetchOffersToProjects(activeDid: string): Promise { const url = `${this.endorserApiBaseUrl}/offers-to-plans`; const headers = await this.getAuthenticatedHeaders(activeDid); const response = await fetch(url, { headers }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); } /** * Fetch project updates with DID-based authentication * * @param activeDid - Active DID for authentication * @param starredPlanIds - Optional list of starred plan IDs * @returns Plans last updated response */ private async fetchProjectUpdates(activeDid: string, starredPlanIds?: string[]): Promise { const url = `${this.endorserApiBaseUrl}/plans-last-updated`; const headers = await this.getAuthenticatedHeaders(activeDid); // Add starred plan IDs as query parameter if provided const params = new URLSearchParams(); if (starredPlanIds && starredPlanIds.length > 0) { params.append('starred', starredPlanIds.join(',')); } const fullUrl = params.toString() ? `${url}?${params.toString()}` : url; const response = await fetch(fullUrl, { headers }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); } /** * Get authenticated headers for API requests * * @param activeDid - Active DID for authentication * @returns Headers with authentication */ private async getAuthenticatedHeaders(activeDid: string): Promise> { const headers: Record = { 'Content-Type': 'application/json', 'Accept': 'application/json' }; // Add DID-based authentication if Veramo stack is available if (this.veramoStack) { try { // Create JWT token with DID const token = await this.createDidJwt(activeDid); headers['Authorization'] = `Bearer ${token}`; } catch (error) { // Failed to create DID JWT, continuing without signature } } return headers; } /** * Create DID-based JWT token * * @param activeDid - Active DID for token creation * @returns JWT token */ private async createDidJwt(activeDid: string): Promise { if (!this.veramoStack) { throw new Error('Veramo stack not initialized'); } // This would integrate with the actual Veramo stack // For now, return a placeholder token // In a real implementation, this would create a JWT with: // - iss: activeDid (issuer) // - sub: activeDid (subject) // - aud: this.endorserApiBaseUrl (audience) // - exp: Math.floor(Date.now() / 1000) + 3600 (1 hour expiration) // - iat: Math.floor(Date.now() / 1000) (issued at) return `placeholder-jwt-${activeDid}`; } /** * Verify DID-signed payload * * @param _payload - Payload to verify * @param _signature - Signature to verify against * @returns Verification result */ async verifyDidSignature(_payload: string, _signature: string): Promise { if (!this.veramoStack) { // Veramo stack not available for signature verification return false; } try { // This would integrate with Veramo's signature verification // For now, return true as placeholder // Verifying DID signature return true; } catch (error) { // Signature verification failed return false; } } /** * Generate sample DID-signed payloads for documentation * * @returns Sample payloads with signatures */ generateSampleDidPayloads(): DidSignedPayload[] { const activeDid = this.activeDid || 'did:example:123'; return [ { type: 'notification_content', payload: { title: 'Daily Community Update', body: 'New offers and project updates available', timestamp: Date.now(), activeDid: activeDid }, signature: 'sample-signature-1', verificationSteps: [ 'Extract DID from payload', 'Resolve DID document', 'Verify signature with public key', 'Check payload expiration' ] }, { type: 'callback_event', payload: { eventType: 'notification_delivered', notificationId: 'notif-123', timestamp: Date.now(), activeDid: activeDid }, signature: 'sample-signature-2', verificationSteps: [ 'Extract DID from payload', 'Resolve DID document', 'Verify signature with public key', 'Validate event type and timestamp' ] } ]; } /** * Get data retention and redaction policy * * @returns Data retention policy */ getDataRetentionPolicy(): DataRetentionPolicy { return { fields: { activeDid: { retention: 'session', redaction: 'hash' }, notificationContent: { retention: '7_days', redaction: 'partial' }, callbackEvents: { retention: '30_days', redaction: 'full' }, performanceMetrics: { retention: '90_days', redaction: 'none' } }, redactionMethods: { hash: 'SHA-256 hash of original value', partial: 'Replace sensitive parts with [REDACTED]', full: 'Replace entire field with [REDACTED]', none: 'No redaction applied' }, storageLocations: { session: 'Memory only, cleared on app close', '7_days': 'Local storage with TTL', '30_days': 'Local storage with TTL', '90_days': 'Local storage with TTL' } }; } } /** * TimeSafari Integration Configuration */ export interface TimeSafariIntegrationConfig { activeDid: string; veramoStack?: unknown; // Veramo stack instance storageAdapter: TimeSafariStorageAdapter; endorserApiBaseUrl?: string; } // Re-export types from definitions export type { TimeSafariUserConfig, TimeSafariNotificationBundle } from './definitions'; /** * TimeSafari Storage Adapter Interface */ export interface TimeSafariStorageAdapter { store(key: string, value: unknown): Promise; retrieve(key: string): Promise; remove(key: string): Promise; clear(): Promise; } /** * DID-signed payload for documentation */ export interface DidSignedPayload { type: string; payload: Record; signature: string; verificationSteps: string[]; } /** * Data retention and redaction policy */ export interface DataRetentionPolicy { fields: Record; redactionMethods: Record; storageLocations: Record; } /** * Field-specific retention policy */ export interface FieldRetentionPolicy { retention: 'session' | '7_days' | '30_days' | '90_days'; redaction: 'hash' | 'partial' | 'full' | 'none'; }