UNPKG

@feedinbox/sdk

Version:

Secure TypeScript SDK for FeedInbox - API client for feedback collection and user engagement

1 lines 37.5 kB
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * FeedInbox SDK - Secure TypeScript client for feedback collection\n * \n * This SDK provides a secure interface to the FeedInbox API with:\n * - Domain validation\n * - API key security\n * - Rate limiting\n * - Retry logic\n * - Input sanitization\n */\n\n// Types\nexport interface FeedInboxConfig {\n apiKey?: string; // Optional - can be provided via env variable\n apiUrl?: string;\n timeout?: number;\n retries?: number;\n customBackendUrl?: string;\n}\n\nexport interface FeedbackData {\n userEmail: string;\n message: string;\n subject?: string;\n priority?: 'low' | 'medium' | 'high';\n workspaceId?: string;\n metadata?: Record<string, any>;\n}\n\nexport interface SubscriptionObject {\n id: string;\n title: string;\n description: string;\n}\n\nexport interface SubscriptionData {\n email: string;\n subscriptions: SubscriptionObject[]; // Array of subscription objects\n consent?: boolean;\n workspaceId?: string;\n metadata?: Record<string, any>;\n}\n\nexport interface ContactData {\n firstName: string;\n lastName?: string;\n email: string;\n subject?: string;\n message: string;\n workspaceId?: string;\n metadata?: Record<string, any>;\n}\n\nexport interface PreferenceObject {\n id: string;\n title: string;\n description: string;\n value?: any;\n}\n\nexport interface PreferencesData {\n preferences: PreferenceObject[]; // Array of preference objects\n workspaceId?: string;\n metadata?: Record<string, any>;\n}\n\nexport interface ApiResponse<T = any> {\n success: boolean;\n data?: T;\n error?: string;\n message?: string;\n}\n\n\nexport interface UnsubscribeInfo {\n email: string;\n subscriptions: SubscriptionObject[];\n active: boolean;\n token: string;\n requires_confirmation: boolean;\n}\n\nexport interface UnsubscribeResult {\n email: string;\n unsubscribed_from: string;\n remaining_subscriptions: SubscriptionObject[];\n completely_unsubscribed: boolean;\n}\n\n// Security utilities\nclass SecurityUtils {\n static validateApiKey(apiKey: string): boolean {\n if (!apiKey || typeof apiKey !== 'string') return false;\n if (!apiKey.startsWith('fb_')) return false;\n if (apiKey.length < 10) return false;\n return true;\n }\n\n static validateEmail(email: string): boolean {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return emailRegex.test(email);\n }\n\n static sanitizeString(str: string): string {\n if (typeof str !== 'string') return '';\n return str.trim().slice(0, 10000); // Max 10k chars\n }\n\n static validateUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n return ['http:', 'https:'].includes(parsed.protocol);\n } catch {\n return false;\n }\n }\n}\n\n// Rate limiter\nclass RateLimiter {\n private requests: number[] = [];\n private readonly maxRequests: number;\n private readonly timeWindow: number;\n\n constructor(maxRequests = 100, timeWindowMs = 60000) {\n this.maxRequests = maxRequests;\n this.timeWindow = timeWindowMs;\n }\n\n canMakeRequest(): boolean {\n const now = Date.now();\n this.requests = this.requests.filter(time => now - time < this.timeWindow);\n \n if (this.requests.length >= this.maxRequests) {\n return false;\n }\n \n this.requests.push(now);\n return true;\n }\n}\n\n// Main SDK class\nexport class FeedInboxSDK {\n private readonly apiKey: string;\n private readonly apiUrl: string;\n private readonly timeout: number;\n private readonly retries: number;\n private readonly rateLimiter: RateLimiter;\n private readonly headers: Record<string, string>;\n\n constructor(config: FeedInboxConfig = {}) {\n // Get API key from config or environment variable\n const apiKey = config.apiKey || this.getApiKeyFromEnv();\n \n if (!apiKey) {\n throw new Error('API key is required. Provide it in config or set FEEDINBOX_API_KEY environment variable');\n }\n\n if (!SecurityUtils.validateApiKey(apiKey)) {\n throw new Error('Invalid API key format. Must start with \"fb_\" and be at least 10 characters');\n }\n\n this.apiKey = apiKey;\n this.apiUrl = config.customBackendUrl || config.apiUrl || this.getDefaultApiUrl();\n this.timeout = config.timeout || 10000; // 10s default\n this.retries = config.retries || 2;\n this.rateLimiter = new RateLimiter();\n\n // Validate API URL\n if (!SecurityUtils.validateUrl(this.apiUrl)) {\n throw new Error('Invalid API URL provided');\n }\n\n this.headers = {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.apiKey}`,\n 'User-Agent': 'FeedInbox-SDK/1.0.0',\n 'X-SDK-Version': '1.0.0',\n // Add Origin header for server-side requests to match domain validation\n 'Origin': typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000',\n };\n }\n\n private getApiKeyFromEnv(): string | undefined {\n // Check for environment variable in both browser and Node.js\n if (typeof process !== 'undefined' && process.env) {\n return process.env.FEEDINBOX_API_KEY;\n }\n \n // In browser environments, check for global variables (set by bundler)\n if (typeof window !== 'undefined' && (window as any).FEEDINBOX_API_KEY) {\n return (window as any).FEEDINBOX_API_KEY;\n }\n \n return undefined;\n }\n\n private getDefaultApiUrl(): string {\n if (typeof window !== 'undefined') {\n // Browser environment - use widget proxy port\n const protocol = window.location.protocol;\n const hostname = window.location.hostname;\n const port = hostname === 'localhost' ? ':3103' : '';\n return `${protocol}//${hostname}${port}`;\n }\n // Node.js environment - default to widget proxy\n return process.env.FEEDINBOX_API_URL || 'http://localhost:3103';\n }\n\n\n private async makeRequest<T>(\n endpoint: string,\n method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' = 'GET',\n data?: any,\n attempt = 1\n ): Promise<ApiResponse<T>> {\n // Rate limiting check\n if (!this.rateLimiter.canMakeRequest()) {\n return {\n success: false,\n error: 'Rate limit exceeded. Please try again later.'\n };\n }\n\n const url = `${this.apiUrl}${endpoint}`;\n \n // Create abort controller for timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n const config: RequestInit = {\n method,\n headers: this.headers,\n signal: controller.signal,\n };\n\n if (data && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {\n config.body = JSON.stringify(data);\n }\n\n // Debug logging removed for security\n\n try {\n const response = await fetch(url, config);\n clearTimeout(timeoutId);\n\n const result = await response.json();\n\n if (!response.ok) {\n // Debug logging removed for security\n \n // Retry on server errors (5xx) and some 4xx errors\n if (attempt < this.retries && (response.status >= 500 || response.status === 429)) {\n await this.delay(Math.pow(2, attempt) * 1000); // Exponential backoff\n return this.makeRequest(endpoint, method, data, attempt + 1);\n }\n\n return {\n success: false,\n error: result.error || `HTTP error! status: ${response.status}`\n };\n }\n\n // Debug logging removed for security\n return result;\n } catch (error) {\n clearTimeout(timeoutId);\n // Debug logging removed for security\n\n // Retry on network errors\n if (attempt < this.retries) {\n await this.delay(Math.pow(2, attempt) * 1000);\n return this.makeRequest(endpoint, method, data, attempt + 1);\n }\n\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Network error'\n };\n }\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n\n // ============================================================================\n // FEEDBACK CRUD OPERATIONS\n // ============================================================================\n\n /**\n * Create feedback entry\n */\n async createFeedback(feedback: FeedbackData): Promise<ApiResponse> {\n if (!feedback.userEmail || !feedback.message) {\n return {\n success: false,\n error: 'userEmail and message are required'\n };\n }\n\n if (!SecurityUtils.validateEmail(feedback.userEmail)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n\n console.log('feedback', feedback);\n\n const sanitizedData = {\n email: SecurityUtils.sanitizeString(feedback.userEmail),\n message: SecurityUtils.sanitizeString(feedback.message),\n subject: feedback.subject ? SecurityUtils.sanitizeString(feedback.subject) : 'Widget Feedback',\n priority: feedback.priority || 'medium',\n workspace_id: feedback.workspaceId,\n page_url: typeof window !== 'undefined' ? window.location.href : undefined,\n user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,\n metadata: feedback.metadata,\n timestamp: new Date().toISOString()\n };\n\n return this.makeRequest('/api/widget/v1/feedback', 'POST', sanitizedData);\n }\n\n /**\n * Get feedback entries\n */\n async getFeedback(params?: { \n userEmail?: string; \n limit?: number; \n page?: number; \n status?: 'open' | 'responded' | 'closed' \n }): Promise<ApiResponse> {\n const queryParams = new URLSearchParams();\n \n if (params?.userEmail) {\n if (!SecurityUtils.validateEmail(params.userEmail)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n queryParams.append('user_email', SecurityUtils.sanitizeString(params.userEmail));\n }\n \n if (params?.limit) queryParams.append('limit', params.limit.toString());\n if (params?.page) queryParams.append('page', params.page.toString());\n if (params?.status) queryParams.append('status', params.status);\n\n const query = queryParams.toString();\n const endpoint = `/api/widget/v1/feedback${query ? `?${query}` : ''}`;\n \n return this.makeRequest(endpoint, 'GET');\n }\n\n /**\n * Update feedback entry\n */\n async updateFeedback(feedbackId: string, updates: Partial<FeedbackData>): Promise<ApiResponse> {\n if (!feedbackId) {\n return {\n success: false,\n error: 'feedbackId is required'\n };\n }\n\n const sanitizedData = {\n feedback_id: SecurityUtils.sanitizeString(feedbackId),\n ...updates,\n timestamp: new Date().toISOString()\n };\n\n return this.makeRequest(`/api/widget/v1/feedback/${feedbackId}`, 'PUT', sanitizedData);\n }\n\n /**\n * Delete feedback entry\n */\n async deleteFeedback(feedbackId: string): Promise<ApiResponse> {\n if (!feedbackId) {\n return {\n success: false,\n error: 'feedbackId is required'\n };\n }\n\n return this.makeRequest(`/api/widget/v1/feedback/${feedbackId}`, 'DELETE');\n }\n\n // ============================================================================\n // SUBSCRIBERS CRUD OPERATIONS (newsletter subscriptions)\n // ============================================================================\n\n /**\n * Create subscriber (newsletter subscription)\n */\n async createSubscriber(subscription: SubscriptionData): Promise<ApiResponse> {\n // Allow empty subscriptions array for \"unsubscribe from all\" operations\n if (!subscription.email || !Array.isArray(subscription.subscriptions)) {\n return {\n success: false,\n error: 'email and subscriptions array are required'\n };\n }\n\n if (!SecurityUtils.validateEmail(subscription.email)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n\n const sanitizedData = {\n email: SecurityUtils.sanitizeString(subscription.email),\n subscriptions: subscription.subscriptions.map(sub => ({\n id: SecurityUtils.sanitizeString(sub.id),\n title: SecurityUtils.sanitizeString(sub.title),\n description: SecurityUtils.sanitizeString(sub.description)\n })),\n workspace_id: subscription.workspaceId,\n page_url: typeof window !== 'undefined' ? window.location.href : undefined,\n consent_given: subscription.consent !== false,\n source: 'widget',\n metadata: subscription.metadata,\n timestamp: new Date().toISOString()\n };\n\n return this.makeRequest('/api/widget/v1/subscribers', 'POST', sanitizedData);\n }\n\n /**\n * Get subscriber data\n */\n async getSubscriber(userEmail: string): Promise<ApiResponse> {\n if (!userEmail) {\n return {\n success: false,\n error: 'userEmail is required'\n };\n }\n\n if (!SecurityUtils.validateEmail(userEmail)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n\n const sanitizedEmail = SecurityUtils.sanitizeString(userEmail);\n return this.makeRequest(`/api/widget/v1/subscribers?email=${encodeURIComponent(sanitizedEmail)}`, 'GET');\n }\n\n /**\n * Update subscriber subscriptions\n */\n async updateSubscriber(userEmail: string, subscriptions: SubscriptionObject[]): Promise<ApiResponse> {\n if (!userEmail) {\n return {\n success: false,\n error: 'userEmail is required'\n };\n }\n\n if (!SecurityUtils.validateEmail(userEmail)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n\n if (!Array.isArray(subscriptions)) {\n return {\n success: false,\n error: 'subscriptions must be an array'\n };\n }\n\n const sanitizedData = {\n email: SecurityUtils.sanitizeString(userEmail),\n subscriptions: subscriptions.map(sub => ({\n id: SecurityUtils.sanitizeString(sub.id),\n title: SecurityUtils.sanitizeString(sub.title),\n description: SecurityUtils.sanitizeString(sub.description)\n })),\n timestamp: new Date().toISOString()\n };\n\n return this.makeRequest('/api/widget/v1/subscribers', 'PUT', sanitizedData);\n }\n\n /**\n * Delete subscriber\n */\n async deleteSubscriber(userEmail: string): Promise<ApiResponse> {\n if (!userEmail) {\n return {\n success: false,\n error: 'userEmail is required'\n };\n }\n\n if (!SecurityUtils.validateEmail(userEmail)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n\n const sanitizedEmail = SecurityUtils.sanitizeString(userEmail);\n return this.makeRequest(`/api/widget/v1/subscribers?email=${encodeURIComponent(sanitizedEmail)}`, 'DELETE');\n }\n\n // ============================================================================\n // CONTACTS CRUD OPERATIONS\n // ============================================================================\n\n /**\n * Create contact form submission\n */\n async createContact(contact: ContactData): Promise<ApiResponse> {\n if (!contact.firstName || !contact.email || !contact.message) {\n return {\n success: false,\n error: 'firstName, email, and message are required'\n };\n }\n\n if (!SecurityUtils.validateEmail(contact.email)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n\n const sanitizedData = {\n first_name: SecurityUtils.sanitizeString(contact.firstName),\n last_name: contact.lastName ? SecurityUtils.sanitizeString(contact.lastName) : undefined,\n email: SecurityUtils.sanitizeString(contact.email),\n subject: contact.subject ? SecurityUtils.sanitizeString(contact.subject) : 'Contact Form Submission',\n message: SecurityUtils.sanitizeString(contact.message),\n workspace_id: contact.workspaceId,\n page_url: typeof window !== 'undefined' ? window.location.href : undefined,\n user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,\n metadata: contact.metadata,\n timestamp: new Date().toISOString()\n };\n\n return this.makeRequest('/api/widget/v1/contact', 'POST', sanitizedData);\n }\n\n /**\n * Get contact submissions\n */\n async getContacts(params?: {\n userEmail?: string;\n limit?: number;\n page?: number;\n }): Promise<ApiResponse> {\n const queryParams = new URLSearchParams();\n \n if (params?.userEmail) {\n if (!SecurityUtils.validateEmail(params.userEmail)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n queryParams.append('user_email', SecurityUtils.sanitizeString(params.userEmail));\n }\n \n if (params?.limit) queryParams.append('limit', params.limit.toString());\n if (params?.page) queryParams.append('page', params.page.toString());\n\n const query = queryParams.toString();\n const endpoint = `/api/widget/v1/contact${query ? `?${query}` : ''}`;\n \n return this.makeRequest(endpoint, 'GET');\n }\n\n /**\n * Update contact submission\n */\n async updateContact(contactId: string, updates: Partial<ContactData>): Promise<ApiResponse> {\n if (!contactId) {\n return {\n success: false,\n error: 'contactId is required'\n };\n }\n\n const sanitizedData = {\n contact_id: SecurityUtils.sanitizeString(contactId),\n ...updates,\n timestamp: new Date().toISOString()\n };\n\n return this.makeRequest(`/api/widget/v1/contact/${contactId}`, 'PUT', sanitizedData);\n }\n\n /**\n * Delete contact submission\n */\n async deleteContact(contactId: string): Promise<ApiResponse> {\n if (!contactId) {\n return {\n success: false,\n error: 'contactId is required'\n };\n }\n\n return this.makeRequest(`/api/widget/v1/contact/${contactId}`, 'DELETE');\n }\n\n // ============================================================================\n // PREFERENCES CRUD OPERATIONS (separate from subscribers!)\n // ============================================================================\n\n /**\n * Create user preferences\n */\n async createPreferences(userEmail: string, preferences: PreferencesData): Promise<ApiResponse> {\n if (!userEmail) {\n return {\n success: false,\n error: 'userEmail is required'\n };\n }\n\n if (!SecurityUtils.validateEmail(userEmail)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n\n // Allow empty preferences array for \"clear all preferences\" operations\n if (!Array.isArray(preferences.preferences)) {\n return {\n success: false,\n error: 'preferences array is required'\n };\n }\n\n const sanitizedData = {\n preferences: preferences.preferences.map(pref => ({\n id: SecurityUtils.sanitizeString(pref.id),\n title: SecurityUtils.sanitizeString(pref.title),\n description: SecurityUtils.sanitizeString(pref.description),\n value: pref.value\n })),\n user_email: SecurityUtils.sanitizeString(userEmail),\n workspace_id: preferences.workspaceId,\n page_url: typeof window !== 'undefined' ? window.location.href : undefined,\n user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,\n metadata: preferences.metadata,\n timestamp: new Date().toISOString()\n };\n\n return this.makeRequest('/api/widget/v1/preferences', 'POST', sanitizedData);\n }\n\n /**\n * Get user preferences\n */\n async getPreferences(userEmail: string): Promise<ApiResponse> {\n if (!userEmail) {\n return {\n success: false,\n error: 'userEmail is required'\n };\n }\n\n if (!SecurityUtils.validateEmail(userEmail)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n\n const sanitizedEmail = SecurityUtils.sanitizeString(userEmail);\n return this.makeRequest(`/api/widget/v1/preferences?user_email=${encodeURIComponent(sanitizedEmail)}`, 'GET');\n }\n\n /**\n * Update user preferences\n */\n async updatePreferences(userEmail: string, preferences: PreferencesData): Promise<ApiResponse> {\n if (!userEmail) {\n return {\n success: false,\n error: 'userEmail is required'\n };\n }\n\n if (!SecurityUtils.validateEmail(userEmail)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n\n // Allow empty preferences array for \"clear all preferences\" operations\n if (!Array.isArray(preferences.preferences)) {\n return {\n success: false,\n error: 'preferences array is required'\n };\n }\n\n const sanitizedData = {\n preferences: preferences.preferences.map(pref => ({\n id: SecurityUtils.sanitizeString(pref.id),\n title: SecurityUtils.sanitizeString(pref.title),\n description: SecurityUtils.sanitizeString(pref.description),\n value: pref.value\n })),\n user_email: SecurityUtils.sanitizeString(userEmail),\n workspace_id: preferences.workspaceId,\n page_url: typeof window !== 'undefined' ? window.location.href : undefined,\n user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,\n metadata: preferences.metadata,\n timestamp: new Date().toISOString()\n };\n\n return this.makeRequest('/api/widget/v1/preferences', 'PUT', sanitizedData);\n }\n\n /**\n * Delete user preferences\n */\n async deletePreferences(userEmail: string): Promise<ApiResponse> {\n if (!userEmail) {\n return {\n success: false,\n error: 'userEmail is required'\n };\n }\n\n if (!SecurityUtils.validateEmail(userEmail)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n\n const sanitizedEmail = SecurityUtils.sanitizeString(userEmail);\n return this.makeRequest(`/api/widget/v1/preferences?user_email=${encodeURIComponent(sanitizedEmail)}`, 'DELETE');\n }\n\n // ============================================================================\n // ESSENTIAL OPTIMIZED METHODS\n // ============================================================================\n\n /**\n * Combined user data fetch - gets preferences, subscriptions, and metadata in one call\n * Optimized to replace multiple separate API calls\n */\n async getUserProfile(userEmail: string, options?: { \n includePreferences?: boolean; \n includeSubscriptions?: boolean; \n includeMetadata?: boolean;\n preferenceIds?: string[];\n subscriptionIds?: string[];\n }): Promise<ApiResponse> {\n if (!userEmail) {\n return {\n success: false,\n error: 'userEmail is required'\n };\n }\n\n if (!SecurityUtils.validateEmail(userEmail)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n\n const sanitizedEmail = SecurityUtils.sanitizeString(userEmail);\n const queryParams = new URLSearchParams();\n queryParams.append('email', sanitizedEmail);\n \n // Build query params based on options\n const opts = options || { includePreferences: true, includeSubscriptions: true };\n if (opts.includePreferences) queryParams.append('include_preferences', 'true');\n if (opts.includeSubscriptions) queryParams.append('include_subscriptions', 'true');\n if (opts.includeMetadata) queryParams.append('include_metadata', 'true');\n if (opts.preferenceIds?.length) queryParams.append('preference_ids', opts.preferenceIds.join(','));\n if (opts.subscriptionIds?.length) queryParams.append('subscription_ids', opts.subscriptionIds.join(','));\n\n return this.makeRequest(`/api/widget/v1/users/profile?${queryParams.toString()}`, 'GET');\n }\n\n /**\n * Check if user has any data (preferences or subscriptions) without fetching full objects\n * Optimized for existence checks\n */\n async hasUserData(userEmail: string): Promise<ApiResponse<{ hasPreferences: boolean; hasSubscriptions: boolean; hasAnyData: boolean }>> {\n if (!userEmail) {\n return {\n success: false,\n error: 'userEmail is required'\n };\n }\n\n if (!SecurityUtils.validateEmail(userEmail)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n\n const sanitizedEmail = SecurityUtils.sanitizeString(userEmail);\n return this.makeRequest(`/api/widget/v1/users/data-check?email=${encodeURIComponent(sanitizedEmail)}`, 'GET');\n }\n\n /**\n * Toggle single subscription on/off by ID - optimized for subscription management\n */\n async toggleSubscription(userEmail: string, subscriptionId: string, enabled: boolean, metadata?: Record<string, any>): Promise<ApiResponse> {\n if (!userEmail || !subscriptionId) {\n return {\n success: false,\n error: 'userEmail and subscriptionId are required'\n };\n }\n\n if (!SecurityUtils.validateEmail(userEmail)) {\n return {\n success: false,\n error: 'Invalid email format'\n };\n }\n\n const sanitizedData = {\n email: SecurityUtils.sanitizeString(userEmail),\n subscription_id: SecurityUtils.sanitizeString(subscriptionId),\n enabled,\n metadata: {\n action: enabled ? 'enable_subscription' : 'disable_subscription',\n ...metadata\n },\n timestamp: new Date().toISOString()\n };\n\n return this.makeRequest('/api/widget/v1/subscribers/toggle', 'PATCH', sanitizedData);\n }\n\n // ============================================================================\n // LEGACY/CONVENIENCE METHODS FOR BACKWARD COMPATIBILITY\n // ============================================================================\n\n /**\n * Subscribe to newsletter (alias for createSubscriber)\n * @deprecated Use createSubscriber instead\n */\n async subscribe(subscription: SubscriptionData): Promise<ApiResponse> {\n return this.createSubscriber(subscription);\n }\n\n /**\n * Submit contact form (alias for createContact)\n * @deprecated Use createContact instead\n */\n async submitContact(contact: ContactData): Promise<ApiResponse> {\n return this.createContact(contact);\n }\n\n\n}\n\n// Export everything\nexport default FeedInboxSDK;"],"mappings":"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","names":["SecurityUtils","apiKey","email","str","url","parsed","RateLimiter","maxRequests","timeWindowMs","__publicField","now","time","FeedInboxSDK","config","protocol","hostname","endpoint","method","data","attempt","controller","timeoutId","response","result","error","ms","resolve","feedback","sanitizedData","params","queryParams","query","feedbackId","updates","subscription","sub","userEmail","sanitizedEmail","subscriptions","contact","contactId","preferences","pref","options","opts","subscriptionId","enabled","metadata","index_default"]}