@feedvalue/core 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- "use strict";var FeedValueCore=(()=>{var p=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var V=Object.prototype.hasOwnProperty;var D=(r,e,t)=>e in r?p(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var I=(r,e)=>{for(var t in e)p(r,t,{get:e[t],enumerable:!0})},R=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of _(e))!V.call(r,s)&&s!==t&&p(r,s,{get:()=>e[s],enumerable:!(i=k(e,s))||i.enumerable});return r};var U=r=>R(p({},"__esModule",{value:!0}),r);var o=(r,e,t)=>D(r,typeof e!="symbol"?e+"":e,t);var B={};I(B,{ApiClient:()=>f,DEFAULT_API_BASE_URL:()=>m,FeedValue:()=>y,TypedEventEmitter:()=>h,clearFingerprint:()=>E,generateFingerprint:()=>v});var h=class{constructor(){o(this,"listeners",new Map)}on(e,t){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t)}once(e,t){let i=((...s)=>{this.off(e,i),t(...s)});this.on(e,i)}off(e,t){if(!t){this.listeners.delete(e);return}let i=this.listeners.get(e);i&&(i.delete(t),i.size===0&&this.listeners.delete(e))}emit(e,...t){let i=this.listeners.get(e);if(i)for(let s of i)try{s(...t)}catch(n){console.error(`[FeedValue] Error in ${e} handler:`,n)}}removeAllListeners(){this.listeners.clear()}};var m="https://api.feedvalue.com";var f=class{constructor(e=m,t=!1){o(this,"baseUrl");o(this,"debug");o(this,"pendingRequests",new Map);o(this,"configCache",new Map);o(this,"submissionToken",null);o(this,"tokenExpiresAt",null);o(this,"fingerprint",null);this.baseUrl=e.replace(/\/$/,""),this.debug=t}validateWidgetId(e){if(!e||typeof e!="string")throw new Error("Widget ID is required");if(!/^[a-zA-Z0-9_-]+$/.test(e))throw new Error("Invalid widget ID format: only alphanumeric characters, underscores, and hyphens are allowed");if(e.length>64)throw new Error("Widget ID exceeds maximum length of 64 characters")}setFingerprint(e){this.fingerprint=e}getFingerprint(){return this.fingerprint}hasValidToken(){return!this.submissionToken||!this.tokenExpiresAt?!1:Date.now()/1e3<this.tokenExpiresAt-30}async fetchConfig(e){this.validateWidgetId(e);let t=`config:${e}`,i=this.configCache.get(t);if(i&&Date.now()<i.expiresAt)return this.log("Config cache hit",{widgetId:e}),i.data;let s=`fetchConfig:${e}`,n=this.pendingRequests.get(s);if(n)return this.log("Deduplicating config request",{widgetId:e}),n;let a=this.doFetchConfig(e);this.pendingRequests.set(s,a);try{return await a}finally{this.pendingRequests.delete(s)}}async doFetchConfig(e){let t=`${this.baseUrl}/api/v1/widgets/${e}/config`,i={};this.fingerprint&&(i["X-Client-Fingerprint"]=this.fingerprint),this.log("Fetching config",{widgetId:e,url:t});let s=await fetch(t,{method:"GET",headers:i});if(!s.ok){let u=await this.parseError(s);throw new Error(u)}let n=await s.json();n.submission_token&&(this.submissionToken=n.submission_token,this.tokenExpiresAt=n.token_expires_at??null,this.log("Submission token stored",{expiresAt:this.tokenExpiresAt?new Date(this.tokenExpiresAt*1e3).toISOString():"unknown"}));let a=`config:${e}`;return this.configCache.set(a,{data:n,expiresAt:Date.now()+3e5}),this.log("Config fetched",{widgetId:e}),n}async submitFeedback(e,t,i){this.validateWidgetId(e);let s=`${this.baseUrl}/api/v1/widgets/${e}/feedback`;if(this.hasValidToken()||(this.log("Token expired, refreshing..."),await this.fetchConfig(e)),!this.submissionToken)throw new Error("No submission token available");let n={"Content-Type":"application/json","X-Submission-Token":this.submissionToken};this.fingerprint&&(n["X-Client-Fingerprint"]=this.fingerprint),this.log("Submitting feedback",{widgetId:e});let a=await fetch(s,{method:"POST",headers:n,body:JSON.stringify({message:t.message,metadata:t.metadata,...t.customFieldValues&&{customFieldValues:t.customFieldValues},...i&&Object.keys(i).length>0&&{user:i}})});if(a.status===429){let l=a.headers.get("X-RateLimit-Reset"),d=l?Math.ceil(parseInt(l,10)-Date.now()/1e3):60;throw new Error(`Rate limited. Try again in ${d} seconds.`)}if(a.status===403){let l=await a.json().catch(()=>({detail:"Access denied"}));if(l.detail?.code&&l.detail?.message)throw new Error(l.detail.message);let d=typeof l.detail=="string"?l.detail:"";if((d.includes("token")||d.includes("expired"))&&(this.log("Token rejected, refreshing..."),this.submissionToken=null,await this.fetchConfig(e),this.submissionToken)){n["X-Submission-Token"]=this.submissionToken;let c=await fetch(s,{method:"POST",headers:n,body:JSON.stringify({message:t.message,metadata:t.metadata,...t.customFieldValues&&{customFieldValues:t.customFieldValues},...i&&Object.keys(i).length>0&&{user:i}})});if(c.ok)return c.json()}throw new Error(d||"Access denied")}if(!a.ok){let l=await this.parseError(a);throw new Error(l)}let u=await a.json();if(u.blocked)throw new Error(u.message||"Unable to submit feedback");return this.log("Feedback submitted",{feedbackId:u.feedback_id}),u}async parseError(e){try{let t=await e.json();return t.detail||t.message||t.error||`HTTP ${e.status}`}catch{return`HTTP ${e.status}: ${e.statusText}`}}clearCache(){this.configCache.clear(),this.submissionToken=null,this.tokenExpiresAt=null,this.log("Cache cleared")}log(e,t){this.debug&&console.log(`[FeedValue API] ${e}`,t??"")}};var x="fv_fingerprint";function C(){if(typeof crypto<"u"&&typeof crypto.randomUUID=="function")return crypto.randomUUID();if(typeof crypto<"u"&&typeof crypto.getRandomValues=="function"){let r=new Uint8Array(16);crypto.getRandomValues(r),r[6]=r[6]&15|64,r[8]=r[8]&63|128;let e=Array.from(r).map(t=>t.toString(16).padStart(2,"0")).join("");return`${e.slice(0,8)}-${e.slice(8,12)}-${e.slice(12,16)}-${e.slice(16,20)}-${e.slice(20)}`}return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,r=>{let e=Math.random()*16|0;return(r==="x"?e:e&3|8).toString(16)})}function v(){if(typeof window>"u"||typeof sessionStorage>"u")return C();let r=sessionStorage.getItem(x);if(r)return r;let e=C();try{sessionStorage.setItem(x,e)}catch{}return e}function E(){if(typeof sessionStorage<"u")try{sessionStorage.removeItem(x)}catch{}}var A=3e3,S=["angry","disappointed","satisfied","excited"],T=1e4,F=1e3,$={theme:"auto",autoShow:!0,debug:!1,locale:"en"},b=new Map,y=class r{constructor(e){o(this,"widgetId");o(this,"apiClient");o(this,"emitter");o(this,"headless");o(this,"config");o(this,"widgetConfig",null);o(this,"state",{isReady:!1,isOpen:!1,isVisible:!0,error:null,isSubmitting:!1});o(this,"stateSubscribers",new Set);o(this,"stateSnapshot");o(this,"_userData",{});o(this,"_userId",null);o(this,"_userTraits",{});o(this,"triggerButton",null);o(this,"modal",null);o(this,"overlay",null);o(this,"stylesInjected",!1);o(this,"autoCloseTimeout",null);this.widgetId=e.widgetId,this.headless=e.headless??!1,this.config={...$,...e.config},this.apiClient=new f(e.apiBaseUrl??m,this.config.debug),this.emitter=new h,this.stateSnapshot={...this.state},this.log("Instance created",{widgetId:this.widgetId,headless:this.headless})}static init(e){let t=b.get(e.widgetId);if(t)return t;let i=new r(e);return b.set(e.widgetId,i),i.init().catch(s=>{console.error("[FeedValue] Initialization failed:",s)}),i}static getInstance(e){return b.get(e)}async init(){if(this.state.isReady){this.log("Already initialized");return}try{this.log("Initializing...");let e=v();this.apiClient.setFingerprint(e);let t=await this.apiClient.fetchConfig(this.widgetId);this.widgetConfig={widgetId:t.widget_id,widgetKey:t.widget_key,appId:"",config:{position:t.config.position??"bottom-right",triggerText:t.config.triggerText??"Feedback",triggerIcon:t.config.triggerIcon??"none",formTitle:t.config.formTitle??"Share your feedback",submitButtonText:t.config.submitButtonText??"Submit",thankYouMessage:t.config.thankYouMessage??"Thank you for your feedback!",showBranding:t.config.showBranding??!0,customFields:t.config.customFields},styling:{primaryColor:t.styling.primaryColor??"#3b82f6",backgroundColor:t.styling.backgroundColor??"#ffffff",textColor:t.styling.textColor??"#1f2937",buttonTextColor:t.styling.buttonTextColor??"#ffffff",borderRadius:t.styling.borderRadius??"8px",customCSS:t.styling.customCSS}},!this.headless&&typeof window<"u"&&typeof document<"u"&&this.renderWidget(),this.updateState({isReady:!0,error:null}),this.emitter.emit("ready"),this.log("Initialized successfully")}catch(e){let t=e instanceof Error?e:new Error(String(e));throw this.updateState({error:t}),this.emitter.emit("error",t),t}}destroy(){this.log("Destroying..."),this.autoCloseTimeout&&(clearTimeout(this.autoCloseTimeout),this.autoCloseTimeout=null),this.triggerButton?.remove(),this.modal?.remove(),this.overlay?.remove(),document.getElementById("fv-widget-styles")?.remove(),document.getElementById("fv-widget-custom-styles")?.remove(),this.triggerButton=null,this.modal=null,this.overlay=null,this.widgetConfig=null,this.stateSubscribers.clear(),this.emitter.removeAllListeners(),this.apiClient.clearCache(),b.delete(this.widgetId),this.state={isReady:!1,isOpen:!1,isVisible:!1,error:null,isSubmitting:!1},this.log("Destroyed")}open(){if(!this.state.isReady){this.log("Cannot open: not ready");return}this.updateState({isOpen:!0}),this.headless||(this.overlay?.classList.add("fv-widget-open"),this.modal?.classList.add("fv-widget-open")),this.emitter.emit("open"),this.log("Opened")}close(){this.updateState({isOpen:!1}),this.headless||(this.overlay?.classList.remove("fv-widget-open"),this.modal?.classList.remove("fv-widget-open")),this.emitter.emit("close"),this.log("Closed")}toggle(){this.state.isOpen?this.close():this.open()}show(){this.updateState({isVisible:!0}),!this.headless&&this.triggerButton&&(this.triggerButton.style.display=""),this.log("Shown")}hide(){this.updateState({isVisible:!1}),!this.headless&&this.triggerButton&&(this.triggerButton.style.display="none"),this.log("Hidden")}isOpen(){return this.state.isOpen}isVisible(){return this.state.isVisible}isReady(){return this.state.isReady}isHeadless(){return this.headless}setData(e){this._userData={...this._userData,...e},this.log("User data set",e)}identify(e,t){this._userId=e,t&&(this._userTraits={...this._userTraits,...t}),this.log("User identified",{userId:e,traits:t})}reset(){this._userData={},this._userId=null,this._userTraits={},this.log("User data reset")}getUserData(){return{userId:this._userId,data:{...this._userData},traits:{...this._userTraits}}}async submit(e){if(!this.state.isReady)throw new Error("Widget not ready");this.validateFeedback(e),this.updateState({isSubmitting:!0});try{let t={message:e.message,sentiment:e.sentiment,customFieldValues:e.customFieldValues,metadata:{page_url:typeof window<"u"?window.location.href:"",referrer:typeof document<"u"?document.referrer:void 0,user_agent:typeof navigator<"u"?navigator.userAgent:void 0,...e.metadata}},i=this.buildSubmissionUserData();await this.apiClient.submitFeedback(this.widgetId,t,i),this.emitter.emit("submit",t),this.log("Feedback submitted",i?{withUserData:!0}:void 0)}catch(t){let i=t instanceof Error?t:new Error(String(t));throw this.emitter.emit("error",i),i}finally{this.updateState({isSubmitting:!1})}}buildSubmissionUserData(){let e=this._userId!==null,t=Object.keys(this._userData).length>0,i=Object.keys(this._userTraits).length>0;if(!e&&!t&&!i)return;let s={};this._userId&&(s.user_id=this._userId);let n=this._userData.email??this._userTraits.email,a=this._userData.name??this._userTraits.name;if(n&&(s.email=n),a&&(s.name=a),i){let{email:u,name:l,...d}=this._userTraits;Object.keys(d).length>0&&(s.traits=d)}if(t){let{email:u,name:l,...d}=this._userData,c={};for(let[g,w]of Object.entries(d))w!==void 0&&(c[g]=w);Object.keys(c).length>0&&(s.custom_data=c)}return s}validateFeedback(e){if(!e.message?.trim())throw new Error("Feedback message is required");if(e.message.length>T)throw new Error(`Feedback message exceeds maximum length of ${T} characters`);if(e.sentiment!==void 0&&!S.includes(e.sentiment))throw new Error(`Invalid sentiment value. Must be one of: ${S.join(", ")}`);if(e.customFieldValues){for(let[t,i]of Object.entries(e.customFieldValues))if(typeof t!="string"||typeof i!="string")throw new Error("Custom field values must be strings")}if(e.metadata){for(let[t,i]of Object.entries(e.metadata))if(typeof i=="string"&&i.length>F)throw new Error(`Metadata field "${t}" exceeds maximum length of ${F} characters`)}}on(e,t){this.emitter.on(e,t)}once(e,t){this.emitter.once(e,t)}off(e,t){this.emitter.off(e,t)}async waitUntilReady(){if(!this.state.isReady){if(this.state.error)throw this.state.error;return new Promise((e,t)=>{this.once("ready",()=>e()),this.once("error",i=>t(i))})}}setConfig(e){this.config={...this.config,...e},this.log("Config updated",e)}getConfig(){return{...this.config}}getWidgetConfig(){return this.widgetConfig}subscribe(e){return this.stateSubscribers.add(e),()=>{this.stateSubscribers.delete(e)}}getSnapshot(){return this.stateSnapshot}updateState(e){this.state={...this.state,...e},this.stateSnapshot={...this.state},this.emitter.emit("stateChange",this.stateSnapshot);for(let t of this.stateSubscribers)t()}renderWidget(){this.widgetConfig&&(this.stylesInjected||(this.injectStyles(),this.stylesInjected=!0),this.renderTrigger(),this.renderModal())}sanitizeCSS(e){let t=[/url\s*\(/gi,/@import/gi,/expression\s*\(/gi,/javascript:/gi,/behavior\s*:/gi,/-moz-binding/gi];for(let i of t)if(i.test(e))return console.warn("[FeedValue] Blocked potentially unsafe CSS pattern"),"";return e}injectStyles(){if(!this.widgetConfig)return;let{styling:e,config:t}=this.widgetConfig,i=document.createElement("style");if(i.id="fv-widget-styles",i.textContent=this.getBaseStyles(e,t.position),document.head.appendChild(i),e.customCSS){let s=this.sanitizeCSS(e.customCSS);if(s){let n=document.createElement("style");n.id="fv-widget-custom-styles",n.textContent=s,document.head.appendChild(n)}}}getBaseStyles(e,t){let i=this.getPositionStyles(t),s=this.getModalPositionStyles(t);return`
1
+ "use strict";var FeedValueCore=(()=>{var p=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var V=Object.prototype.hasOwnProperty;var D=(o,e,t)=>e in o?p(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var R=(o,e)=>{for(var t in e)p(o,t,{get:e[t],enumerable:!0})},I=(o,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of _(e))!V.call(o,s)&&s!==t&&p(o,s,{get:()=>e[s],enumerable:!(i=k(e,s))||i.enumerable});return o};var A=o=>I(p({},"__esModule",{value:!0}),o);var r=(o,e,t)=>D(o,typeof e!="symbol"?e+"":e,t);var O={};R(O,{ApiClient:()=>f,DEFAULT_API_BASE_URL:()=>m,FeedValue:()=>w,TypedEventEmitter:()=>h,clearFingerprint:()=>E,generateFingerprint:()=>b});var h=class{constructor(){r(this,"listeners",new Map)}on(e,t){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t)}once(e,t){let i=((...s)=>{this.off(e,i),t(...s)});this.on(e,i)}off(e,t){if(!t){this.listeners.delete(e);return}let i=this.listeners.get(e);i&&(i.delete(t),i.size===0&&this.listeners.delete(e))}emit(e,...t){let i=this.listeners.get(e);if(i)for(let s of i)try{s(...t)}catch(n){console.error(`[FeedValue] Error in ${e} handler:`,n)}}removeAllListeners(){this.listeners.clear()}};var m="https://api.feedvalue.com";var f=class{constructor(e=m,t=!1){r(this,"baseUrl");r(this,"debug");r(this,"pendingRequests",new Map);r(this,"configCache",new Map);r(this,"submissionToken",null);r(this,"tokenExpiresAt",null);r(this,"fingerprint",null);this.baseUrl=e.replace(/\/$/,""),this.debug=t}validateWidgetId(e){if(!e||typeof e!="string")throw new Error("Widget ID is required");if(!/^[a-zA-Z0-9_-]+$/.test(e))throw new Error("Invalid widget ID format: only alphanumeric characters, underscores, and hyphens are allowed");if(e.length>64)throw new Error("Widget ID exceeds maximum length of 64 characters")}setFingerprint(e){this.fingerprint=e}getFingerprint(){return this.fingerprint}hasValidToken(){return!this.submissionToken||!this.tokenExpiresAt?!1:Date.now()/1e3<this.tokenExpiresAt-30}async fetchConfig(e,t=!1){this.validateWidgetId(e);let i=`config:${e}`;if(t)this.log("Bypassing cache for token refresh",{widgetId:e});else{let d=this.configCache.get(i);if(d&&Date.now()<d.expiresAt)return this.log("Config cache hit",{widgetId:e}),d.data}let s=`fetchConfig:${e}`,n=this.pendingRequests.get(s);if(n)return this.log("Deduplicating config request",{widgetId:e}),n;let l=this.doFetchConfig(e);this.pendingRequests.set(s,l);try{return await l}finally{this.pendingRequests.delete(s)}}async doFetchConfig(e){let t=`${this.baseUrl}/api/v1/widgets/${e}/config`,i={};this.fingerprint&&(i["X-Client-Fingerprint"]=this.fingerprint),this.log("Fetching config",{widgetId:e,url:t,hasFingerprint:!!this.fingerprint,fingerprintPreview:this.fingerprint?`${this.fingerprint.substring(0,8)}...`:null});let s=await fetch(t,{method:"GET",headers:i});if(!s.ok){let d=await this.parseError(s);throw new Error(d)}let n=await s.json();n.submission_token?(this.submissionToken=n.submission_token,this.tokenExpiresAt=n.token_expires_at??null,this.log("Submission token stored",{expiresAt:this.tokenExpiresAt?new Date(this.tokenExpiresAt*1e3).toISOString():"unknown"})):this.log("No submission token in response",{hasWidgetId:!!n.widget_id,hasConfig:!!n.config,responseKeys:Object.keys(n)});let l=`config:${e}`;return this.configCache.set(l,{data:n,expiresAt:Date.now()+3e5}),this.log("Config fetched",{widgetId:e}),n}async submitFeedback(e,t,i){this.validateWidgetId(e);let s=`${this.baseUrl}/api/v1/widgets/${e}/feedback`;if(this.hasValidToken()||(this.log("Token expired, refreshing..."),await this.fetchConfig(e,!0)),!this.submissionToken)throw new Error("No submission token available");let n={"Content-Type":"application/json","X-Submission-Token":this.submissionToken};this.fingerprint&&(n["X-Client-Fingerprint"]=this.fingerprint),this.log("Submitting feedback",{widgetId:e});let l={...t.metadata,...i&&Object.keys(i).length>0&&{user:i}},d=await fetch(s,{method:"POST",headers:n,body:JSON.stringify({message:t.message,metadata:Object.keys(l).length>0?l:void 0,...t.customFieldValues&&{customFieldValues:t.customFieldValues}})});if(d.status===429){let a=d.headers.get("X-RateLimit-Reset"),u=a?Math.ceil(parseInt(a,10)-Date.now()/1e3):60;throw new Error(`Rate limited. Try again in ${u} seconds.`)}if(d.status===403){let a=await d.json().catch(()=>({detail:"Access denied"}));if(a.detail?.code&&a.detail?.message)throw new Error(a.detail.message);let u=typeof a.detail=="string"?a.detail:"";if((u.includes("token")||u.includes("expired"))&&(this.log("Token rejected, refreshing..."),this.submissionToken=null,await this.fetchConfig(e,!0),this.submissionToken)){n["X-Submission-Token"]=this.submissionToken;let g=await fetch(s,{method:"POST",headers:n,body:JSON.stringify({message:t.message,metadata:t.metadata,...t.customFieldValues&&{customFieldValues:t.customFieldValues},...i&&Object.keys(i).length>0&&{user:i}})});if(g.ok)return g.json()}throw new Error(u||"Access denied")}if(!d.ok){let a=await this.parseError(d);throw new Error(a)}let c=await d.json();if(c.blocked)throw new Error(c.message||"Unable to submit feedback");return this.log("Feedback submitted",{feedbackId:c.feedback_id}),c}async parseError(e){try{let t=await e.json();return t.detail||t.message||t.error||`HTTP ${e.status}`}catch{return`HTTP ${e.status}: ${e.statusText}`}}clearCache(){this.configCache.clear(),this.submissionToken=null,this.tokenExpiresAt=null,this.log("Cache cleared")}log(e,t){this.debug&&console.log(`[FeedValue API] ${e}`,t??"")}};var v="fv_fingerprint";function C(){if(typeof crypto>"u"||typeof crypto.getRandomValues!="function")throw new Error("crypto.getRandomValues is required but not available. Ensure you are running in a modern browser or Node.js 15+.");let o=new Uint8Array(16);return crypto.getRandomValues(o),Array.from(o).map(e=>e.toString(16).padStart(2,"0")).join("")}function b(){if(typeof window>"u"||typeof sessionStorage>"u")return C();let o=sessionStorage.getItem(v);if(o){if(o.includes("-")){let t=o.replace(/-/g,"");try{sessionStorage.setItem(v,t)}catch{}return t}return o}let e=C();try{sessionStorage.setItem(v,e)}catch{}return e}function E(){if(typeof sessionStorage<"u")try{sessionStorage.removeItem(v)}catch{}}var U=3e3,S=["angry","disappointed","satisfied","excited"],T=1e4,F=1e3,B={theme:"auto",autoShow:!0,debug:!1,locale:"en"},y=new Map,w=class o{constructor(e){r(this,"widgetId");r(this,"apiClient");r(this,"emitter");r(this,"headless");r(this,"config");r(this,"widgetConfig",null);r(this,"state",{isReady:!1,isOpen:!1,isVisible:!0,error:null,isSubmitting:!1});r(this,"stateSubscribers",new Set);r(this,"stateSnapshot");r(this,"_userData",{});r(this,"_userId",null);r(this,"_userTraits",{});r(this,"triggerButton",null);r(this,"modal",null);r(this,"overlay",null);r(this,"stylesInjected",!1);r(this,"autoCloseTimeout",null);this.widgetId=e.widgetId,this.headless=e.headless??!1,this.config={...B,...e.config},this.apiClient=new f(e.apiBaseUrl??m,this.config.debug),this.emitter=new h,this.stateSnapshot={...this.state},this.log("Instance created",{widgetId:this.widgetId,headless:this.headless})}static init(e){let t=y.get(e.widgetId);if(t)return t;let i=new o(e);return y.set(e.widgetId,i),i.init().catch(s=>{console.error("[FeedValue] Initialization failed:",s)}),i}static getInstance(e){return y.get(e)}async init(){if(this.state.isReady){this.log("Already initialized");return}try{this.log("Initializing...");let e=b();this.apiClient.setFingerprint(e);let t=await this.apiClient.fetchConfig(this.widgetId);this.widgetConfig={widgetId:t.widget_id,widgetKey:t.widget_key,appId:"",config:{position:t.config.position??"bottom-right",triggerText:t.config.triggerText??"Feedback",triggerIcon:t.config.triggerIcon??"none",formTitle:t.config.formTitle??"Share your feedback",submitButtonText:t.config.submitButtonText??"Submit",thankYouMessage:t.config.thankYouMessage??"Thank you for your feedback!",showBranding:t.config.showBranding??!0,customFields:t.config.customFields},styling:{primaryColor:t.styling.primaryColor??"#3b82f6",backgroundColor:t.styling.backgroundColor??"#ffffff",textColor:t.styling.textColor??"#1f2937",buttonTextColor:t.styling.buttonTextColor??"#ffffff",borderRadius:t.styling.borderRadius??"8px",customCSS:t.styling.customCSS}},!this.headless&&typeof window<"u"&&typeof document<"u"&&this.renderWidget(),this.updateState({isReady:!0,error:null}),this.emitter.emit("ready"),this.log("Initialized successfully")}catch(e){let t=e instanceof Error?e:new Error(String(e));throw this.updateState({error:t}),this.emitter.emit("error",t),t}}destroy(){this.log("Destroying..."),this.autoCloseTimeout&&(clearTimeout(this.autoCloseTimeout),this.autoCloseTimeout=null),this.triggerButton?.remove(),this.modal?.remove(),this.overlay?.remove(),document.getElementById("fv-widget-styles")?.remove(),document.getElementById("fv-widget-custom-styles")?.remove(),this.triggerButton=null,this.modal=null,this.overlay=null,this.widgetConfig=null,this.stateSubscribers.clear(),this.emitter.removeAllListeners(),this.apiClient.clearCache(),y.delete(this.widgetId),this.state={isReady:!1,isOpen:!1,isVisible:!1,error:null,isSubmitting:!1},this.log("Destroyed")}open(){if(!this.state.isReady){this.log("Cannot open: not ready");return}this.updateState({isOpen:!0}),this.headless||(this.overlay?.classList.add("fv-widget-open"),this.modal?.classList.add("fv-widget-open")),this.emitter.emit("open"),this.log("Opened")}close(){this.updateState({isOpen:!1}),this.headless||(this.overlay?.classList.remove("fv-widget-open"),this.modal?.classList.remove("fv-widget-open")),this.emitter.emit("close"),this.log("Closed")}toggle(){this.state.isOpen?this.close():this.open()}show(){this.updateState({isVisible:!0}),!this.headless&&this.triggerButton&&(this.triggerButton.style.display=""),this.log("Shown")}hide(){this.updateState({isVisible:!1}),!this.headless&&this.triggerButton&&(this.triggerButton.style.display="none"),this.log("Hidden")}isOpen(){return this.state.isOpen}isVisible(){return this.state.isVisible}isReady(){return this.state.isReady}isHeadless(){return this.headless}setData(e){this._userData={...this._userData,...e},this.log("User data set",e)}identify(e,t){this._userId=e,t&&(this._userTraits={...this._userTraits,...t}),this.log("User identified",{userId:e,traits:t})}reset(){this._userData={},this._userId=null,this._userTraits={},this.log("User data reset")}getUserData(){return{userId:this._userId,data:{...this._userData},traits:{...this._userTraits}}}async submit(e){if(!this.state.isReady)throw new Error("Widget not ready");this.validateFeedback(e),this.updateState({isSubmitting:!0});try{let t={message:e.message,sentiment:e.sentiment,customFieldValues:e.customFieldValues,metadata:{page_url:typeof window<"u"?window.location.href:"",referrer:typeof document<"u"?document.referrer:void 0,user_agent:typeof navigator<"u"?navigator.userAgent:void 0,...e.metadata}},i=this.buildSubmissionUserData();await this.apiClient.submitFeedback(this.widgetId,t,i),this.emitter.emit("submit",t),this.log("Feedback submitted",i?{withUserData:!0}:void 0)}catch(t){let i=t instanceof Error?t:new Error(String(t));throw this.emitter.emit("error",i),i}finally{this.updateState({isSubmitting:!1})}}buildSubmissionUserData(){let e=this._userId!==null,t=Object.keys(this._userData).length>0,i=Object.keys(this._userTraits).length>0;if(!e&&!t&&!i)return;let s={};this._userId&&(s.user_id=this._userId);let n=this._userData.email??this._userTraits.email,l=this._userData.name??this._userTraits.name;if(n&&(s.email=n),l&&(s.name=l),i){let{email:d,name:c,...a}=this._userTraits;Object.keys(a).length>0&&(s.traits=a)}if(t){let{email:d,name:c,...a}=this._userData,u={};for(let[g,x]of Object.entries(a))x!==void 0&&(u[g]=x);Object.keys(u).length>0&&(s.custom_data=u)}return s}validateFeedback(e){if(!e.message?.trim())throw new Error("Feedback message is required");if(e.message.length>T)throw new Error(`Feedback message exceeds maximum length of ${T} characters`);if(e.sentiment!==void 0&&!S.includes(e.sentiment))throw new Error(`Invalid sentiment value. Must be one of: ${S.join(", ")}`);if(e.customFieldValues){for(let[t,i]of Object.entries(e.customFieldValues))if(typeof t!="string"||typeof i!="string")throw new Error("Custom field values must be strings")}if(e.metadata){for(let[t,i]of Object.entries(e.metadata))if(typeof i=="string"&&i.length>F)throw new Error(`Metadata field "${t}" exceeds maximum length of ${F} characters`)}}on(e,t){this.emitter.on(e,t)}once(e,t){this.emitter.once(e,t)}off(e,t){this.emitter.off(e,t)}async waitUntilReady(){if(!this.state.isReady){if(this.state.error)throw this.state.error;return new Promise((e,t)=>{this.once("ready",()=>e()),this.once("error",i=>t(i))})}}setConfig(e){this.config={...this.config,...e},this.log("Config updated",e)}getConfig(){return{...this.config}}getWidgetConfig(){return this.widgetConfig}subscribe(e){return this.stateSubscribers.add(e),()=>{this.stateSubscribers.delete(e)}}getSnapshot(){return this.stateSnapshot}updateState(e){this.state={...this.state,...e},this.stateSnapshot={...this.state},this.emitter.emit("stateChange",this.stateSnapshot);for(let t of this.stateSubscribers)t()}renderWidget(){this.widgetConfig&&(this.stylesInjected||(this.injectStyles(),this.stylesInjected=!0),this.renderTrigger(),this.renderModal())}sanitizeCSS(e){let t=[/url\s*\(/gi,/@import/gi,/expression\s*\(/gi,/javascript:/gi,/behavior\s*:/gi,/-moz-binding/gi];for(let i of t)if(i.test(e))return console.warn("[FeedValue] Blocked potentially unsafe CSS pattern"),"";return e}injectStyles(){if(!this.widgetConfig)return;let{styling:e,config:t}=this.widgetConfig,i=document.createElement("style");if(i.id="fv-widget-styles",i.textContent=this.getBaseStyles(e,t.position),document.head.appendChild(i),e.customCSS){let s=this.sanitizeCSS(e.customCSS);if(s){let n=document.createElement("style");n.id="fv-widget-custom-styles",n.textContent=s,document.head.appendChild(n)}}}getBaseStyles(e,t){let i=this.getPositionStyles(t),s=this.getModalPositionStyles(t);return`
2
2
  .fv-widget-trigger {
3
3
  position: fixed;
4
4
  ${i}
@@ -134,5 +134,5 @@
134
134
  color: ${e.primaryColor};
135
135
  text-decoration: none;
136
136
  }
137
- `}getPositionStyles(e){switch(e){case"bottom-left":return"bottom: 20px; left: 20px;";case"top-right":return"top: 20px; right: 20px;";case"top-left":return"top: 20px; left: 20px;";case"center":return"top: 50%; left: 50%; transform: translate(-50%, -50%);";default:return"bottom: 20px; right: 20px;"}}getModalPositionStyles(e){switch(e){case"bottom-left":return"bottom: 20px; left: 20px;";case"bottom-right":return"bottom: 20px; right: 20px;";case"top-right":return"top: 20px; right: 20px;";case"top-left":return"top: 20px; left: 20px;";default:return"top: 50%; left: 50%; transform: translate(-50%, -50%);"}}renderTrigger(){this.widgetConfig&&(this.triggerButton=document.createElement("button"),this.triggerButton.className="fv-widget-trigger",this.triggerButton.textContent=this.widgetConfig.config.triggerText,this.triggerButton.addEventListener("click",()=>this.open()),document.body.appendChild(this.triggerButton))}renderModal(){if(!this.widgetConfig)return;let{config:e}=this.widgetConfig;this.overlay=document.createElement("div"),this.overlay.className="fv-widget-overlay",this.overlay.addEventListener("click",()=>this.close()),document.body.appendChild(this.overlay),this.modal=document.createElement("div"),this.modal.className="fv-widget-modal";let t=document.createElement("div");t.className="fv-widget-header";let i=document.createElement("h2");i.className="fv-widget-title",i.textContent=e.formTitle,t.appendChild(i);let s=document.createElement("button");s.className="fv-widget-close",s.setAttribute("aria-label","Close"),s.textContent="\xD7",s.addEventListener("click",()=>this.close()),t.appendChild(s),this.modal.appendChild(t);let n=document.createElement("form");n.className="fv-widget-form",n.id="fv-feedback-form";let a=document.createElement("textarea");a.className="fv-widget-textarea",a.id="fv-feedback-content",a.placeholder="Tell us what you think...",a.required=!0,n.appendChild(a);let u=document.createElement("button");u.type="submit",u.className="fv-widget-submit",u.textContent=e.submitButtonText,n.appendChild(u);let l=document.createElement("div");if(l.className="fv-widget-error",l.id="fv-error-message",n.appendChild(l),n.addEventListener("submit",d=>this.handleFormSubmit(d)),this.modal.appendChild(n),e.showBranding){let d=document.createElement("div");d.className="fv-widget-branding";let c=document.createTextNode("Powered by ");d.appendChild(c);let g=document.createElement("a");g.href="https://feedvalue.com",g.target="_blank",g.rel="noopener noreferrer",g.textContent="FeedValue",d.appendChild(g),this.modal.appendChild(d)}document.body.appendChild(this.modal)}async handleFormSubmit(e){e.preventDefault();let t=document.getElementById("fv-feedback-content"),i=this.modal?.querySelector(".fv-widget-submit"),s=document.getElementById("fv-error-message");if(!t?.value.trim()){this.showError("Please enter your feedback");return}try{i.disabled=!0,i.textContent="Submitting...",s&&(s.style.display="none"),await this.submit({message:t.value.trim()}),this.showSuccess()}catch(n){let a=n instanceof Error?n.message:"Failed to submit";this.showError(a)}finally{i.disabled=!1,i.textContent=this.widgetConfig?.config.submitButtonText??"Submit"}}showError(e){let t=document.getElementById("fv-error-message");t&&(t.textContent=e,t.style.display="block")}showSuccess(){if(!this.modal||!this.widgetConfig)return;this.autoCloseTimeout&&(clearTimeout(this.autoCloseTimeout),this.autoCloseTimeout=null),this.modal.textContent="";let e=document.createElement("div");e.style.cssText="text-align: center; padding: 40px 20px;";let t=document.createElement("div");t.style.cssText="font-size: 48px; margin-bottom: 16px;",t.textContent="\u2713",e.appendChild(t);let i=document.createElement("div");i.style.cssText="font-size: 16px; margin-bottom: 24px;",i.textContent=this.widgetConfig.config.thankYouMessage,e.appendChild(i);let s=document.createElement("button");s.className="fv-widget-submit",s.textContent="Close",s.addEventListener("click",()=>{this.close(),this.resetForm()}),e.appendChild(s),this.modal.appendChild(e),this.autoCloseTimeout=setTimeout(()=>{this.state.isOpen&&(this.close(),this.resetForm()),this.autoCloseTimeout=null},A)}resetForm(){this.modal&&(this.modal.textContent="",this.modal.remove(),this.modal=null,this.renderModal())}log(e,t){this.config.debug&&console.log(`[FeedValue] ${e}`,t??"")}};return U(B);})();
137
+ `}getPositionStyles(e){switch(e){case"bottom-left":return"bottom: 20px; left: 20px;";case"top-right":return"top: 20px; right: 20px;";case"top-left":return"top: 20px; left: 20px;";case"center":return"top: 50%; left: 50%; transform: translate(-50%, -50%);";default:return"bottom: 20px; right: 20px;"}}getModalPositionStyles(e){switch(e){case"bottom-left":return"bottom: 20px; left: 20px;";case"bottom-right":return"bottom: 20px; right: 20px;";case"top-right":return"top: 20px; right: 20px;";case"top-left":return"top: 20px; left: 20px;";default:return"top: 50%; left: 50%; transform: translate(-50%, -50%);"}}renderTrigger(){this.widgetConfig&&(this.triggerButton=document.createElement("button"),this.triggerButton.className="fv-widget-trigger",this.triggerButton.textContent=this.widgetConfig.config.triggerText,this.triggerButton.addEventListener("click",()=>this.open()),document.body.appendChild(this.triggerButton))}renderModal(){if(!this.widgetConfig)return;let{config:e}=this.widgetConfig;this.overlay=document.createElement("div"),this.overlay.className="fv-widget-overlay",this.overlay.addEventListener("click",()=>this.close()),document.body.appendChild(this.overlay),this.modal=document.createElement("div"),this.modal.className="fv-widget-modal";let t=document.createElement("div");t.className="fv-widget-header";let i=document.createElement("h2");i.className="fv-widget-title",i.textContent=e.formTitle,t.appendChild(i);let s=document.createElement("button");s.className="fv-widget-close",s.setAttribute("aria-label","Close"),s.textContent="\xD7",s.addEventListener("click",()=>this.close()),t.appendChild(s),this.modal.appendChild(t);let n=document.createElement("form");n.className="fv-widget-form",n.id="fv-feedback-form";let l=document.createElement("textarea");l.className="fv-widget-textarea",l.id="fv-feedback-content",l.placeholder="Tell us what you think...",l.required=!0,n.appendChild(l);let d=document.createElement("button");d.type="submit",d.className="fv-widget-submit",d.textContent=e.submitButtonText,n.appendChild(d);let c=document.createElement("div");if(c.className="fv-widget-error",c.id="fv-error-message",n.appendChild(c),n.addEventListener("submit",a=>this.handleFormSubmit(a)),this.modal.appendChild(n),e.showBranding){let a=document.createElement("div");a.className="fv-widget-branding";let u=document.createTextNode("Powered by ");a.appendChild(u);let g=document.createElement("a");g.href="https://feedvalue.com",g.target="_blank",g.rel="noopener noreferrer",g.textContent="FeedValue",a.appendChild(g),this.modal.appendChild(a)}document.body.appendChild(this.modal)}async handleFormSubmit(e){e.preventDefault();let t=document.getElementById("fv-feedback-content"),i=this.modal?.querySelector(".fv-widget-submit"),s=document.getElementById("fv-error-message");if(!t?.value.trim()){this.showError("Please enter your feedback");return}try{i.disabled=!0,i.textContent="Submitting...",s&&(s.style.display="none"),await this.submit({message:t.value.trim()}),this.showSuccess()}catch(n){let l=n instanceof Error?n.message:"Failed to submit";this.showError(l)}finally{i.disabled=!1,i.textContent=this.widgetConfig?.config.submitButtonText??"Submit"}}showError(e){let t=document.getElementById("fv-error-message");t&&(t.textContent=e,t.style.display="block")}showSuccess(){if(!this.modal||!this.widgetConfig)return;this.autoCloseTimeout&&(clearTimeout(this.autoCloseTimeout),this.autoCloseTimeout=null),this.modal.textContent="";let e=document.createElement("div");e.style.cssText="text-align: center; padding: 40px 20px;";let t=document.createElement("div");t.style.cssText="font-size: 48px; margin-bottom: 16px;",t.textContent="\u2713",e.appendChild(t);let i=document.createElement("div");i.style.cssText="font-size: 16px; margin-bottom: 24px;",i.textContent=this.widgetConfig.config.thankYouMessage,e.appendChild(i);let s=document.createElement("button");s.className="fv-widget-submit",s.textContent="Close",s.addEventListener("click",()=>{this.close(),this.resetForm()}),e.appendChild(s),this.modal.appendChild(e),this.autoCloseTimeout=setTimeout(()=>{this.state.isOpen&&(this.close(),this.resetForm()),this.autoCloseTimeout=null},U)}resetForm(){this.modal&&(this.modal.textContent="",this.modal.remove(),this.modal=null,this.renderModal())}log(e,t){this.config.debug&&console.log(`[FeedValue] ${e}`,t??"")}};return A(O);})();
138
138
  //# sourceMappingURL=index.min.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/index.ts","../../src/event-emitter.ts","../../src/api-client.ts","../../src/fingerprint.ts","../../src/feedvalue.ts"],"sourcesContent":["/**\n * @feedvalue/core\n *\n * Core FeedValue SDK for JavaScript/TypeScript applications.\n *\n * @example\n * ```ts\n * import { FeedValue } from '@feedvalue/core';\n *\n * // Initialize widget\n * const widget = FeedValue.init({ widgetId: 'your-widget-id' });\n *\n * // Control widget\n * widget.open();\n * widget.close();\n *\n * // Set user data\n * widget.setData({ email: 'user@example.com' });\n * widget.identify('user-123', { plan: 'pro' });\n *\n * // Listen to events\n * widget.on('submit', (feedback) => {\n * console.log('Feedback submitted:', feedback);\n * });\n *\n * // Programmatic submission\n * await widget.submit({ message: 'Great product!' });\n * ```\n *\n * @packageDocumentation\n */\n\n// Main class\nexport { FeedValue } from './feedvalue';\n\n// Types\nexport type {\n // Configuration\n FeedValueOptions,\n FeedValueConfig,\n WidgetConfig,\n WidgetPosition,\n WidgetTheme,\n WidgetUIConfig,\n WidgetStyling,\n TriggerIconType,\n\n // Custom Fields\n CustomField,\n CustomFieldType,\n EmojiSentiment,\n\n // State\n FeedValueState,\n\n // Feedback\n FeedbackData,\n FeedbackMetadata,\n\n // Events\n FeedValueEvents,\n EventHandler,\n\n // User Data\n UserData,\n UserTraits,\n\n // Instance Interface\n FeedValueInstance,\n\n // API Types (for advanced usage)\n ConfigResponse,\n FeedbackResponse,\n SubmissionUserData,\n} from './types';\n\n// Event Emitter (for advanced usage)\nexport { TypedEventEmitter } from './event-emitter';\n\n// API Client (for advanced usage)\nexport { ApiClient, DEFAULT_API_BASE_URL } from './api-client';\n\n// Fingerprint (for advanced usage)\nexport { generateFingerprint, clearFingerprint } from './fingerprint';\n","/**\n * @feedvalue/core - Type-Safe Event Emitter\n *\n * A minimal, type-safe event emitter for FeedValue events.\n * Provides compile-time type checking for event names and handlers.\n */\n\nimport type { FeedValueEvents, EventHandler } from './types';\n\n/**\n * Type-safe event emitter for FeedValue events\n *\n * @example\n * ```ts\n * const emitter = new TypedEventEmitter();\n *\n * emitter.on('ready', () => console.log('Ready!'));\n * emitter.on('submit', (feedback) => console.log('Submitted:', feedback));\n *\n * emitter.emit('ready');\n * emitter.emit('submit', { message: 'Great app!' });\n * ```\n */\nexport class TypedEventEmitter {\n // Using a Map with proper typing - the inner Function type is acceptable here\n // because we handle type safety at the public API level (on/off/emit methods)\n private listeners = new Map<keyof FeedValueEvents, Set<(...args: unknown[]) => void>>();\n\n /**\n * Subscribe to an event\n *\n * @param event - Event name\n * @param handler - Event handler function\n */\n on<K extends keyof FeedValueEvents>(event: K, handler: EventHandler<K>): void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n this.listeners.get(event)!.add(handler as (...args: unknown[]) => void);\n }\n\n /**\n * Subscribe to an event for a single emission.\n * The handler will be automatically removed after the first call.\n *\n * @param event - Event name\n * @param handler - Event handler function\n */\n once<K extends keyof FeedValueEvents>(event: K, handler: EventHandler<K>): void {\n const wrappedHandler = ((...args: unknown[]) => {\n this.off(event, wrappedHandler as EventHandler<K>);\n (handler as (...args: unknown[]) => void)(...args);\n }) as EventHandler<K>;\n\n this.on(event, wrappedHandler);\n }\n\n /**\n * Unsubscribe from an event\n *\n * @param event - Event name\n * @param handler - Optional handler to remove (removes all if not provided)\n */\n off<K extends keyof FeedValueEvents>(event: K, handler?: EventHandler<K>): void {\n if (!handler) {\n // Remove all handlers for this event\n this.listeners.delete(event);\n return;\n }\n\n const handlers = this.listeners.get(event);\n if (handlers) {\n handlers.delete(handler as (...args: unknown[]) => void);\n if (handlers.size === 0) {\n this.listeners.delete(event);\n }\n }\n }\n\n /**\n * Emit an event to all subscribers\n *\n * @param event - Event name\n * @param args - Arguments to pass to handlers\n */\n emit<K extends keyof FeedValueEvents>(\n event: K,\n ...args: Parameters<EventHandler<K>>\n ): void {\n const handlers = this.listeners.get(event);\n if (handlers) {\n for (const handler of handlers) {\n try {\n handler(...args);\n } catch (error) {\n // Prevent one handler's error from breaking others\n console.error(`[FeedValue] Error in ${event} handler:`, error);\n }\n }\n }\n }\n\n /**\n * Remove all event listeners\n */\n removeAllListeners(): void {\n this.listeners.clear();\n }\n}\n","/**\n * @feedvalue/core - API Client\n *\n * Handles all communication with the FeedValue API.\n * Includes request deduplication, caching, and error handling.\n */\n\nimport type { ConfigResponse, FeedbackResponse, FeedbackData, SubmissionUserData } from './types';\n\n/**\n * Default API base URL\n */\nexport const DEFAULT_API_BASE_URL = 'https://api.feedvalue.com';\n\n/** Buffer time before token is considered expired (seconds) */\nconst TOKEN_EXPIRY_BUFFER_SECONDS = 30;\n\n/** How long to cache widget config (milliseconds) */\nconst CONFIG_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes\n\n/**\n * Cache entry with TTL\n */\ninterface CacheEntry<T> {\n data: T;\n expiresAt: number;\n}\n\n/**\n * API client for FeedValue\n */\nexport class ApiClient {\n private baseUrl: string;\n private debug: boolean;\n\n // Request deduplication\n private pendingRequests = new Map<string, Promise<unknown>>();\n\n // Config cache\n private configCache = new Map<string, CacheEntry<ConfigResponse>>();\n\n // Anti-abuse tokens\n private submissionToken: string | null = null;\n private tokenExpiresAt: number | null = null;\n private fingerprint: string | null = null;\n\n constructor(baseUrl: string = DEFAULT_API_BASE_URL, debug = false) {\n this.baseUrl = baseUrl.replace(/\\/$/, ''); // Remove trailing slash\n this.debug = debug;\n }\n\n /**\n * Validate widget ID to prevent path injection attacks\n * @throws Error if widget ID is invalid\n */\n private validateWidgetId(widgetId: string): void {\n if (!widgetId || typeof widgetId !== 'string') {\n throw new Error('Widget ID is required');\n }\n if (!/^[a-zA-Z0-9_-]+$/.test(widgetId)) {\n throw new Error('Invalid widget ID format: only alphanumeric characters, underscores, and hyphens are allowed');\n }\n if (widgetId.length > 64) {\n throw new Error('Widget ID exceeds maximum length of 64 characters');\n }\n }\n\n /**\n * Set client fingerprint for anti-abuse protection\n */\n setFingerprint(fingerprint: string): void {\n this.fingerprint = fingerprint;\n }\n\n /**\n * Get client fingerprint\n */\n getFingerprint(): string | null {\n return this.fingerprint;\n }\n\n /**\n * Check if submission token is valid\n */\n hasValidToken(): boolean {\n if (!this.submissionToken || !this.tokenExpiresAt) {\n return false;\n }\n // Add buffer before expiry\n return Date.now() / 1000 < this.tokenExpiresAt - TOKEN_EXPIRY_BUFFER_SECONDS;\n }\n\n /**\n * Fetch widget configuration\n * Uses caching and request deduplication\n */\n async fetchConfig(widgetId: string): Promise<ConfigResponse> {\n this.validateWidgetId(widgetId);\n const cacheKey = `config:${widgetId}`;\n\n // Check cache first\n const cached = this.configCache.get(cacheKey);\n if (cached && Date.now() < cached.expiresAt) {\n this.log('Config cache hit', { widgetId });\n return cached.data;\n }\n\n // Deduplicate concurrent requests\n const pendingKey = `fetchConfig:${widgetId}`;\n const pending = this.pendingRequests.get(pendingKey);\n if (pending) {\n this.log('Deduplicating config request', { widgetId });\n return pending as Promise<ConfigResponse>;\n }\n\n const request = this.doFetchConfig(widgetId);\n this.pendingRequests.set(pendingKey, request);\n\n try {\n const result = await request;\n return result;\n } finally {\n this.pendingRequests.delete(pendingKey);\n }\n }\n\n /**\n * Actually fetch config from API\n */\n private async doFetchConfig(widgetId: string): Promise<ConfigResponse> {\n const url = `${this.baseUrl}/api/v1/widgets/${widgetId}/config`;\n\n const headers: Record<string, string> = {};\n if (this.fingerprint) {\n headers['X-Client-Fingerprint'] = this.fingerprint;\n }\n\n this.log('Fetching config', { widgetId, url });\n\n const response = await fetch(url, {\n method: 'GET',\n headers,\n });\n\n if (!response.ok) {\n const error = await this.parseError(response);\n throw new Error(error);\n }\n\n const data = await response.json() as ConfigResponse;\n\n // Store submission token\n if (data.submission_token) {\n this.submissionToken = data.submission_token;\n this.tokenExpiresAt = data.token_expires_at ?? null;\n this.log('Submission token stored', {\n expiresAt: this.tokenExpiresAt ? new Date(this.tokenExpiresAt * 1000).toISOString() : 'unknown',\n });\n }\n\n // Cache the response\n const cacheKey = `config:${widgetId}`;\n this.configCache.set(cacheKey, {\n data,\n expiresAt: Date.now() + CONFIG_CACHE_TTL_MS,\n });\n\n this.log('Config fetched', { widgetId });\n return data;\n }\n\n /**\n * Submit feedback with optional user data\n */\n async submitFeedback(\n widgetId: string,\n feedback: FeedbackData,\n userData?: SubmissionUserData\n ): Promise<FeedbackResponse> {\n this.validateWidgetId(widgetId);\n const url = `${this.baseUrl}/api/v1/widgets/${widgetId}/feedback`;\n\n // Refresh token if needed\n if (!this.hasValidToken()) {\n this.log('Token expired, refreshing...');\n await this.fetchConfig(widgetId);\n }\n\n if (!this.submissionToken) {\n throw new Error('No submission token available');\n }\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-Submission-Token': this.submissionToken,\n };\n\n if (this.fingerprint) {\n headers['X-Client-Fingerprint'] = this.fingerprint;\n }\n\n this.log('Submitting feedback', { widgetId });\n\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n message: feedback.message,\n metadata: feedback.metadata,\n ...(feedback.customFieldValues && {\n customFieldValues: feedback.customFieldValues,\n }),\n ...(userData && Object.keys(userData).length > 0 && {\n user: userData,\n }),\n }),\n });\n\n // Handle rate limiting\n if (response.status === 429) {\n const resetAt = response.headers.get('X-RateLimit-Reset');\n const retryAfter = resetAt\n ? Math.ceil(parseInt(resetAt, 10) - Date.now() / 1000)\n : 60;\n throw new Error(`Rate limited. Try again in ${retryAfter} seconds.`);\n }\n\n // Handle forbidden (token issues)\n if (response.status === 403) {\n const errorData = await response.json().catch(() => ({ detail: 'Access denied' }));\n\n // Check for subscription limit\n if (errorData.detail?.code && errorData.detail?.message) {\n throw new Error(errorData.detail.message);\n }\n\n // Try token refresh and retry once\n const errorMessage = typeof errorData.detail === 'string' ? errorData.detail : '';\n if (errorMessage.includes('token') || errorMessage.includes('expired')) {\n this.log('Token rejected, refreshing...');\n this.submissionToken = null;\n await this.fetchConfig(widgetId);\n\n if (this.submissionToken) {\n // Retry with new token\n headers['X-Submission-Token'] = this.submissionToken;\n const retryResponse = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n message: feedback.message,\n metadata: feedback.metadata,\n ...(feedback.customFieldValues && {\n customFieldValues: feedback.customFieldValues,\n }),\n ...(userData && Object.keys(userData).length > 0 && {\n user: userData,\n }),\n }),\n });\n\n if (retryResponse.ok) {\n return retryResponse.json();\n }\n }\n }\n\n throw new Error(errorMessage || 'Access denied');\n }\n\n if (!response.ok) {\n const error = await this.parseError(response);\n throw new Error(error);\n }\n\n const data = await response.json() as FeedbackResponse;\n\n // Check for soft blocks\n if (data.blocked) {\n throw new Error(data.message || 'Unable to submit feedback');\n }\n\n this.log('Feedback submitted', { feedbackId: data.feedback_id });\n return data;\n }\n\n /**\n * Parse error from response\n */\n private async parseError(response: Response): Promise<string> {\n try {\n const data = await response.json();\n return data.detail || data.message || data.error || `HTTP ${response.status}`;\n } catch {\n return `HTTP ${response.status}: ${response.statusText}`;\n }\n }\n\n /**\n * Clear all caches\n */\n clearCache(): void {\n this.configCache.clear();\n this.submissionToken = null;\n this.tokenExpiresAt = null;\n this.log('Cache cleared');\n }\n\n /**\n * Debug logging\n */\n private log(message: string, data?: Record<string, unknown>): void {\n if (this.debug) {\n console.log(`[FeedValue API] ${message}`, data ?? '');\n }\n }\n}\n","/**\n * Simple fingerprint generation for anti-abuse protection.\n * Uses a session-based UUID stored in sessionStorage.\n *\n * This replaces the previous complex fingerprinting system (canvas, WebGL, audio)\n * which was overkill for an MVP feedback widget and raised privacy concerns.\n */\n\n/**\n * Storage key for the fingerprint\n */\nconst FINGERPRINT_STORAGE_KEY = 'fv_fingerprint';\n\n/**\n * Generate a UUID v4.\n * Uses crypto.randomUUID() if available, otherwise falls back to a manual implementation.\n */\nfunction generateUUID(): string {\n // Use native crypto.randomUUID if available\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n\n // Fallback: manual UUID v4 generation using crypto.getRandomValues\n if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n\n // Set version (4) and variant (RFC 4122)\n // Using non-null assertions since we know the array has 16 elements\n bytes[6] = (bytes[6]! & 0x0f) | 0x40;\n bytes[8] = (bytes[8]! & 0x3f) | 0x80;\n\n const hex = Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n }\n\n // Last resort fallback using Math.random (less secure but functional)\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Generate or retrieve a session fingerprint.\n *\n * The fingerprint is:\n * - Generated once per browser session using crypto.randomUUID()\n * - Stored in sessionStorage for consistency within a session\n * - Automatically cleared when the browser tab/window is closed\n *\n * @returns A unique fingerprint string for the current session\n */\nexport function generateFingerprint(): string {\n // Check for SSR environment\n if (typeof window === 'undefined' || typeof sessionStorage === 'undefined') {\n return generateUUID();\n }\n\n // Try to get existing fingerprint from session\n const stored = sessionStorage.getItem(FINGERPRINT_STORAGE_KEY);\n if (stored) {\n return stored;\n }\n\n // Generate new fingerprint\n const fingerprint = generateUUID();\n\n try {\n sessionStorage.setItem(FINGERPRINT_STORAGE_KEY, fingerprint);\n } catch {\n // sessionStorage may be unavailable (private browsing, quota exceeded)\n // Fall through and return the generated fingerprint anyway\n }\n\n return fingerprint;\n}\n\n/**\n * Clear the stored fingerprint.\n * Useful for testing or when user requests data reset.\n */\nexport function clearFingerprint(): void {\n if (typeof sessionStorage !== 'undefined') {\n try {\n sessionStorage.removeItem(FINGERPRINT_STORAGE_KEY);\n } catch {\n // Ignore errors\n }\n }\n}\n","/**\n * @feedvalue/core - FeedValue Class\n *\n * Main FeedValue SDK class. Provides the public API for interacting\n * with the feedback widget.\n *\n * For vanilla JavaScript usage, this class also handles DOM rendering.\n * Framework packages (React, Vue) use this class as a headless core.\n */\n\nimport type {\n FeedValueOptions,\n FeedValueConfig,\n FeedValueState,\n FeedValueInstance,\n FeedValueEvents,\n EventHandler,\n UserData,\n UserTraits,\n FeedbackData,\n WidgetConfig,\n EmojiSentiment,\n SubmissionUserData,\n} from './types';\nimport { TypedEventEmitter } from './event-emitter';\nimport { ApiClient, DEFAULT_API_BASE_URL } from './api-client';\nimport { generateFingerprint } from './fingerprint';\n\n/** Delay before auto-closing the success message (milliseconds) */\nconst SUCCESS_AUTO_CLOSE_DELAY_MS = 3000;\n\n/**\n * Valid sentiment values for feedback validation\n */\nconst VALID_SENTIMENTS: readonly EmojiSentiment[] = ['angry', 'disappointed', 'satisfied', 'excited'] as const;\n\n/**\n * Maximum allowed message length (10,000 characters)\n */\nconst MAX_MESSAGE_LENGTH = 10000;\n\n/**\n * Maximum allowed length for metadata string values (1,000 characters)\n */\nconst MAX_METADATA_VALUE_LENGTH = 1000;\n\n/**\n * Default configuration\n */\nconst DEFAULT_CONFIG: FeedValueConfig = {\n theme: 'auto',\n autoShow: true,\n debug: false,\n locale: 'en',\n};\n\n/**\n * Instance registry for singleton per widgetId\n */\nconst instances = new Map<string, FeedValue>();\n\n/**\n * FeedValue SDK\n *\n * @example\n * ```ts\n * // Initialize\n * const feedvalue = FeedValue.init({ widgetId: 'abc123' });\n *\n * // Control\n * feedvalue.open();\n * feedvalue.close();\n *\n * // User data\n * feedvalue.setData({ email: 'user@example.com' });\n * feedvalue.identify('user-123', { plan: 'pro' });\n *\n * // Events\n * feedvalue.on('submit', (feedback) => {\n * console.log('Feedback submitted:', feedback);\n * });\n * ```\n */\nexport class FeedValue implements FeedValueInstance {\n private readonly widgetId: string;\n private readonly apiClient: ApiClient;\n private readonly emitter: TypedEventEmitter;\n private readonly headless: boolean;\n private config: FeedValueConfig;\n private widgetConfig: WidgetConfig | null = null;\n\n // State\n private state: FeedValueState = {\n isReady: false,\n isOpen: false,\n isVisible: true,\n error: null,\n isSubmitting: false,\n };\n\n // State subscribers (for React useSyncExternalStore)\n private stateSubscribers = new Set<() => void>();\n private stateSnapshot: FeedValueState;\n\n // User data (stored for future API submissions)\n private _userData: UserData = {};\n private _userId: string | null = null;\n private _userTraits: UserTraits = {};\n\n // DOM elements (for vanilla usage)\n private triggerButton: HTMLElement | null = null;\n private modal: HTMLElement | null = null;\n private overlay: HTMLElement | null = null;\n private stylesInjected = false;\n\n // Auto-close timeout reference (for cleanup on destroy)\n private autoCloseTimeout: ReturnType<typeof setTimeout> | null = null;\n\n /**\n * Create a new FeedValue instance\n * Use FeedValue.init() for public API\n */\n private constructor(options: FeedValueOptions) {\n this.widgetId = options.widgetId;\n this.headless = options.headless ?? false;\n this.config = { ...DEFAULT_CONFIG, ...options.config };\n\n this.apiClient = new ApiClient(\n options.apiBaseUrl ?? DEFAULT_API_BASE_URL,\n this.config.debug\n );\n\n this.emitter = new TypedEventEmitter();\n this.stateSnapshot = { ...this.state };\n\n this.log('Instance created', { widgetId: this.widgetId, headless: this.headless });\n }\n\n /**\n * Initialize FeedValue\n * Returns existing instance if already initialized for this widgetId\n */\n static init(options: FeedValueOptions): FeedValue {\n // Return existing instance if available\n const existing = instances.get(options.widgetId);\n if (existing) {\n return existing;\n }\n\n // Create new instance\n const instance = new FeedValue(options);\n instances.set(options.widgetId, instance);\n\n // Auto-initialize\n instance.init().catch((error) => {\n console.error('[FeedValue] Initialization failed:', error);\n });\n\n return instance;\n }\n\n /**\n * Get existing instance by widgetId\n */\n static getInstance(widgetId: string): FeedValue | undefined {\n return instances.get(widgetId);\n }\n\n // ===========================================================================\n // Lifecycle\n // ===========================================================================\n\n /**\n * Initialize the widget\n */\n async init(): Promise<void> {\n if (this.state.isReady) {\n this.log('Already initialized');\n return;\n }\n\n try {\n this.log('Initializing...');\n\n // Generate fingerprint\n const fingerprint = generateFingerprint();\n this.apiClient.setFingerprint(fingerprint);\n\n // Fetch config\n const configResponse = await this.apiClient.fetchConfig(this.widgetId);\n\n // Build widget config\n this.widgetConfig = {\n widgetId: configResponse.widget_id,\n widgetKey: configResponse.widget_key,\n appId: '',\n config: {\n position: configResponse.config.position ?? 'bottom-right',\n triggerText: configResponse.config.triggerText ?? 'Feedback',\n triggerIcon: configResponse.config.triggerIcon ?? 'none',\n formTitle: configResponse.config.formTitle ?? 'Share your feedback',\n submitButtonText: configResponse.config.submitButtonText ?? 'Submit',\n thankYouMessage: configResponse.config.thankYouMessage ?? 'Thank you for your feedback!',\n showBranding: configResponse.config.showBranding ?? true,\n customFields: configResponse.config.customFields,\n },\n styling: {\n primaryColor: configResponse.styling.primaryColor ?? '#3b82f6',\n backgroundColor: configResponse.styling.backgroundColor ?? '#ffffff',\n textColor: configResponse.styling.textColor ?? '#1f2937',\n buttonTextColor: configResponse.styling.buttonTextColor ?? '#ffffff',\n borderRadius: configResponse.styling.borderRadius ?? '8px',\n customCSS: configResponse.styling.customCSS,\n },\n };\n\n // Render DOM (for vanilla usage, skip in headless mode)\n if (!this.headless && typeof window !== 'undefined' && typeof document !== 'undefined') {\n this.renderWidget();\n }\n\n // Update state\n this.updateState({ isReady: true, error: null });\n this.emitter.emit('ready');\n\n this.log('Initialized successfully');\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n this.updateState({ error: err });\n this.emitter.emit('error', err);\n throw err;\n }\n }\n\n /**\n * Destroy the widget\n */\n destroy(): void {\n this.log('Destroying...');\n\n // Clear auto-close timeout to prevent memory leak\n if (this.autoCloseTimeout) {\n clearTimeout(this.autoCloseTimeout);\n this.autoCloseTimeout = null;\n }\n\n // Remove DOM elements\n this.triggerButton?.remove();\n this.modal?.remove();\n this.overlay?.remove();\n document.getElementById('fv-widget-styles')?.remove();\n document.getElementById('fv-widget-custom-styles')?.remove();\n\n // Clear references\n this.triggerButton = null;\n this.modal = null;\n this.overlay = null;\n this.widgetConfig = null;\n\n // Clear state\n this.stateSubscribers.clear();\n this.emitter.removeAllListeners();\n this.apiClient.clearCache();\n\n // Remove from registry\n instances.delete(this.widgetId);\n\n // Reset state\n this.state = {\n isReady: false,\n isOpen: false,\n isVisible: false,\n error: null,\n isSubmitting: false,\n };\n\n this.log('Destroyed');\n }\n\n // ===========================================================================\n // Widget Control\n // ===========================================================================\n\n open(): void {\n if (!this.state.isReady) {\n this.log('Cannot open: not ready');\n return;\n }\n\n this.updateState({ isOpen: true });\n\n // Only manipulate DOM in non-headless mode\n if (!this.headless) {\n this.overlay?.classList.add('fv-widget-open');\n this.modal?.classList.add('fv-widget-open');\n }\n\n this.emitter.emit('open');\n this.log('Opened');\n }\n\n close(): void {\n this.updateState({ isOpen: false });\n\n // Only manipulate DOM in non-headless mode\n if (!this.headless) {\n this.overlay?.classList.remove('fv-widget-open');\n this.modal?.classList.remove('fv-widget-open');\n }\n\n this.emitter.emit('close');\n this.log('Closed');\n }\n\n toggle(): void {\n if (this.state.isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n show(): void {\n this.updateState({ isVisible: true });\n\n // Only manipulate DOM in non-headless mode\n if (!this.headless && this.triggerButton) {\n this.triggerButton.style.display = '';\n }\n\n this.log('Shown');\n }\n\n hide(): void {\n this.updateState({ isVisible: false });\n\n // Only manipulate DOM in non-headless mode\n if (!this.headless && this.triggerButton) {\n this.triggerButton.style.display = 'none';\n }\n\n this.log('Hidden');\n }\n\n // ===========================================================================\n // State Queries\n // ===========================================================================\n\n isOpen(): boolean {\n return this.state.isOpen;\n }\n\n isVisible(): boolean {\n return this.state.isVisible;\n }\n\n isReady(): boolean {\n return this.state.isReady;\n }\n\n isHeadless(): boolean {\n return this.headless;\n }\n\n // ===========================================================================\n // User Data\n // ===========================================================================\n\n setData(data: Partial<UserData>): void {\n this._userData = { ...this._userData, ...data };\n this.log('User data set', data);\n }\n\n identify(userId: string, traits?: UserTraits): void {\n this._userId = userId;\n if (traits) {\n this._userTraits = { ...this._userTraits, ...traits };\n }\n this.log('User identified', { userId, traits });\n }\n\n reset(): void {\n this._userData = {};\n this._userId = null;\n this._userTraits = {};\n this.log('User data reset');\n }\n\n /**\n * Get current user data (for debugging/testing)\n */\n getUserData(): { userId: string | null; data: UserData; traits: UserTraits } {\n return {\n userId: this._userId,\n data: { ...this._userData },\n traits: { ...this._userTraits },\n };\n }\n\n // ===========================================================================\n // Feedback\n // ===========================================================================\n\n async submit(feedback: Partial<FeedbackData>): Promise<void> {\n if (!this.state.isReady) {\n throw new Error('Widget not ready');\n }\n\n // Validate feedback data before submission\n this.validateFeedback(feedback);\n\n this.updateState({ isSubmitting: true });\n\n try {\n const fullFeedback: FeedbackData = {\n message: feedback.message!,\n sentiment: feedback.sentiment,\n customFieldValues: feedback.customFieldValues,\n metadata: {\n page_url: typeof window !== 'undefined' ? window.location.href : '',\n referrer: typeof document !== 'undefined' ? document.referrer : undefined,\n user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,\n ...feedback.metadata,\n },\n };\n\n // Build user data for submission if any has been set\n const userData = this.buildSubmissionUserData();\n\n await this.apiClient.submitFeedback(this.widgetId, fullFeedback, userData);\n\n this.emitter.emit('submit', fullFeedback);\n this.log('Feedback submitted', userData ? { withUserData: true } : undefined);\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n this.emitter.emit('error', err);\n throw err;\n } finally {\n this.updateState({ isSubmitting: false });\n }\n }\n\n /**\n * Build user data object for API submission\n * Combines data from identify() and setData() calls\n */\n private buildSubmissionUserData(): SubmissionUserData | undefined {\n const hasUserId = this._userId !== null;\n const hasUserData = Object.keys(this._userData).length > 0;\n const hasTraits = Object.keys(this._userTraits).length > 0;\n\n // No user data set, return undefined\n if (!hasUserId && !hasUserData && !hasTraits) {\n return undefined;\n }\n\n const result: SubmissionUserData = {};\n\n // Add user ID if set via identify()\n if (this._userId) {\n result.user_id = this._userId;\n }\n\n // Extract email and name from userData (setData) or traits (identify)\n const email = this._userData.email ?? (this._userTraits.email as string | undefined);\n const name = this._userData.name ?? (this._userTraits.name as string | undefined);\n\n if (email) result.email = email;\n if (name) result.name = name;\n\n // Add traits (excluding email/name which are top-level)\n if (hasTraits) {\n const { email: _e, name: _n, ...otherTraits } = this._userTraits;\n if (Object.keys(otherTraits).length > 0) {\n result.traits = otherTraits;\n }\n }\n\n // Add custom data from setData() (excluding email/name which are top-level)\n if (hasUserData) {\n const { email: _e, name: _n, ...customData } = this._userData;\n // Filter out undefined values\n const filtered: Record<string, string> = {};\n for (const [key, value] of Object.entries(customData)) {\n if (value !== undefined) {\n filtered[key] = value;\n }\n }\n if (Object.keys(filtered).length > 0) {\n result.custom_data = filtered;\n }\n }\n\n return result;\n }\n\n /**\n * Validate feedback data before submission\n * @throws Error if validation fails\n */\n private validateFeedback(feedback: Partial<FeedbackData>): void {\n // Validate message is present and not empty\n if (!feedback.message?.trim()) {\n throw new Error('Feedback message is required');\n }\n\n // Validate message length\n if (feedback.message.length > MAX_MESSAGE_LENGTH) {\n throw new Error(`Feedback message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`);\n }\n\n // Validate sentiment if provided\n if (feedback.sentiment !== undefined && !VALID_SENTIMENTS.includes(feedback.sentiment)) {\n throw new Error(`Invalid sentiment value. Must be one of: ${VALID_SENTIMENTS.join(', ')}`);\n }\n\n // Validate customFieldValues are string key-value pairs\n if (feedback.customFieldValues) {\n for (const [key, value] of Object.entries(feedback.customFieldValues)) {\n if (typeof key !== 'string' || typeof value !== 'string') {\n throw new Error('Custom field values must be strings');\n }\n }\n }\n\n // Validate metadata field lengths\n if (feedback.metadata) {\n for (const [key, value] of Object.entries(feedback.metadata)) {\n if (typeof value === 'string' && value.length > MAX_METADATA_VALUE_LENGTH) {\n throw new Error(`Metadata field \"${key}\" exceeds maximum length of ${MAX_METADATA_VALUE_LENGTH} characters`);\n }\n }\n }\n }\n\n // ===========================================================================\n // Events\n // ===========================================================================\n\n on<K extends keyof FeedValueEvents>(event: K, callback: EventHandler<K>): void {\n this.emitter.on(event, callback);\n }\n\n /**\n * Subscribe to an event for a single emission.\n * The handler will be automatically removed after the first call.\n */\n once<K extends keyof FeedValueEvents>(event: K, callback: EventHandler<K>): void {\n this.emitter.once(event, callback);\n }\n\n off<K extends keyof FeedValueEvents>(event: K, callback?: EventHandler<K>): void {\n this.emitter.off(event, callback);\n }\n\n /**\n * Returns a promise that resolves when the widget is ready.\n * Useful for programmatic initialization flows.\n *\n * @throws {Error} If initialization fails\n */\n async waitUntilReady(): Promise<void> {\n if (this.state.isReady) {\n return;\n }\n\n if (this.state.error) {\n throw this.state.error;\n }\n\n return new Promise((resolve, reject) => {\n this.once('ready', () => resolve());\n this.once('error', (error) => reject(error));\n });\n }\n\n // ===========================================================================\n // Configuration\n // ===========================================================================\n\n setConfig(config: Partial<FeedValueConfig>): void {\n this.config = { ...this.config, ...config };\n this.log('Config updated', config);\n }\n\n getConfig(): FeedValueConfig {\n return { ...this.config };\n }\n\n /**\n * Get widget configuration (from API)\n */\n getWidgetConfig(): WidgetConfig | null {\n return this.widgetConfig;\n }\n\n // ===========================================================================\n // Framework Integration\n // ===========================================================================\n\n /**\n * Subscribe to state changes\n * Used by React's useSyncExternalStore\n */\n subscribe(callback: () => void): () => void {\n this.stateSubscribers.add(callback);\n return () => {\n this.stateSubscribers.delete(callback);\n };\n }\n\n /**\n * Get current state snapshot\n * Used by React's useSyncExternalStore\n */\n getSnapshot(): FeedValueState {\n return this.stateSnapshot;\n }\n\n // ===========================================================================\n // Internal Methods\n // ===========================================================================\n\n /**\n * Update state and notify subscribers\n */\n private updateState(partial: Partial<FeedValueState>): void {\n this.state = { ...this.state, ...partial };\n // Create new snapshot (important for React)\n this.stateSnapshot = { ...this.state };\n this.emitter.emit('stateChange', this.stateSnapshot);\n // Notify subscribers\n for (const subscriber of this.stateSubscribers) {\n subscriber();\n }\n }\n\n /**\n * Render widget DOM elements (for vanilla usage)\n */\n private renderWidget(): void {\n if (!this.widgetConfig) return;\n\n // Inject styles once\n if (!this.stylesInjected) {\n this.injectStyles();\n this.stylesInjected = true;\n }\n\n // Create trigger button\n this.renderTrigger();\n\n // Create modal\n this.renderModal();\n }\n\n /**\n * Sanitize CSS to block potentially dangerous patterns\n * Prevents CSS injection attacks via url(), @import, and other vectors\n */\n private sanitizeCSS(css: string): string {\n // Block potentially dangerous CSS patterns\n const BLOCKED_PATTERNS = [\n /url\\s*\\(/gi, // External resources\n /@import/gi, // External stylesheets\n /expression\\s*\\(/gi, // IE expressions\n /javascript:/gi, // JavaScript URLs\n /behavior\\s*:/gi, // IE behaviors\n /-moz-binding/gi, // Firefox XBL\n ];\n\n for (const pattern of BLOCKED_PATTERNS) {\n if (pattern.test(css)) {\n console.warn('[FeedValue] Blocked potentially unsafe CSS pattern');\n return ''; // Block entire CSS if unsafe pattern found\n }\n }\n return css;\n }\n\n /**\n * Inject CSS styles\n */\n private injectStyles(): void {\n if (!this.widgetConfig) return;\n\n const { styling, config } = this.widgetConfig;\n\n const styleEl = document.createElement('style');\n styleEl.id = 'fv-widget-styles';\n styleEl.textContent = this.getBaseStyles(styling, config.position);\n document.head.appendChild(styleEl);\n\n // Custom CSS - sanitize to prevent CSS injection attacks\n if (styling.customCSS) {\n const sanitizedCSS = this.sanitizeCSS(styling.customCSS);\n if (sanitizedCSS) {\n const customStyleEl = document.createElement('style');\n customStyleEl.id = 'fv-widget-custom-styles';\n customStyleEl.textContent = sanitizedCSS;\n document.head.appendChild(customStyleEl);\n }\n }\n }\n\n /**\n * Get base CSS styles\n */\n private getBaseStyles(styling: WidgetConfig['styling'], position: string): string {\n const positionStyles = this.getPositionStyles(position);\n const modalPositionStyles = this.getModalPositionStyles(position);\n\n return `\n .fv-widget-trigger {\n position: fixed;\n ${positionStyles}\n background-color: ${styling.primaryColor};\n color: ${styling.buttonTextColor};\n padding: 12px 24px;\n border-radius: ${styling.borderRadius};\n cursor: pointer;\n z-index: 9998;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 14px;\n font-weight: 500;\n border: none;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n transition: transform 0.2s, box-shadow 0.2s;\n }\n .fv-widget-trigger:hover {\n transform: translateY(-2px);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);\n }\n .fv-widget-overlay {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgba(0, 0, 0, 0.5);\n z-index: 9998;\n display: none;\n backdrop-filter: blur(4px);\n }\n .fv-widget-overlay.fv-widget-open {\n display: block;\n }\n .fv-widget-modal {\n position: fixed;\n ${modalPositionStyles}\n background-color: ${styling.backgroundColor};\n color: ${styling.textColor};\n border-radius: ${styling.borderRadius};\n padding: 24px;\n max-width: 500px;\n width: 90%;\n max-height: 90vh;\n overflow-y: auto;\n z-index: 9999;\n display: none;\n box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);\n font-family: system-ui, -apple-system, sans-serif;\n }\n .fv-widget-modal.fv-widget-open {\n display: block;\n }\n .fv-widget-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 20px;\n }\n .fv-widget-title {\n font-size: 20px;\n font-weight: 600;\n margin: 0;\n }\n .fv-widget-close {\n background: transparent;\n border: none;\n font-size: 24px;\n cursor: pointer;\n color: ${styling.textColor};\n padding: 0;\n width: 32px;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .fv-widget-form {\n display: flex;\n flex-direction: column;\n gap: 16px;\n }\n .fv-widget-textarea {\n width: 100%;\n min-height: 120px;\n padding: 12px;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: ${styling.borderRadius};\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 14px;\n resize: vertical;\n box-sizing: border-box;\n background-color: ${styling.backgroundColor};\n color: ${styling.textColor};\n }\n .fv-widget-textarea:focus {\n outline: none;\n border-color: ${styling.primaryColor};\n box-shadow: 0 0 0 2px ${styling.primaryColor}33;\n }\n .fv-widget-submit {\n background-color: ${styling.primaryColor};\n color: ${styling.buttonTextColor};\n padding: 12px 24px;\n border: none;\n border-radius: ${styling.borderRadius};\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: opacity 0.2s;\n }\n .fv-widget-submit:hover:not(:disabled) {\n opacity: 0.9;\n }\n .fv-widget-submit:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n .fv-widget-error {\n color: #ef4444;\n font-size: 14px;\n margin-top: 8px;\n display: none;\n }\n .fv-widget-branding {\n text-align: center;\n font-size: 12px;\n color: ${styling.textColor};\n margin-top: 16px;\n opacity: 0.7;\n }\n .fv-widget-branding a {\n color: ${styling.primaryColor};\n text-decoration: none;\n }\n `;\n }\n\n /**\n * Get trigger button position styles\n */\n private getPositionStyles(position: string): string {\n switch (position) {\n case 'bottom-left':\n return 'bottom: 20px; left: 20px;';\n case 'top-right':\n return 'top: 20px; right: 20px;';\n case 'top-left':\n return 'top: 20px; left: 20px;';\n case 'center':\n return 'top: 50%; left: 50%; transform: translate(-50%, -50%);';\n case 'bottom-right':\n default:\n return 'bottom: 20px; right: 20px;';\n }\n }\n\n /**\n * Get modal position styles\n */\n private getModalPositionStyles(position: string): string {\n switch (position) {\n case 'bottom-left':\n return 'bottom: 20px; left: 20px;';\n case 'bottom-right':\n return 'bottom: 20px; right: 20px;';\n case 'top-right':\n return 'top: 20px; right: 20px;';\n case 'top-left':\n return 'top: 20px; left: 20px;';\n case 'center':\n default:\n return 'top: 50%; left: 50%; transform: translate(-50%, -50%);';\n }\n }\n\n /**\n * Render trigger button using safe DOM methods\n */\n private renderTrigger(): void {\n if (!this.widgetConfig) return;\n\n this.triggerButton = document.createElement('button');\n this.triggerButton.className = 'fv-widget-trigger';\n // textContent is safe - no HTML parsing\n this.triggerButton.textContent = this.widgetConfig.config.triggerText;\n this.triggerButton.addEventListener('click', () => this.open());\n\n document.body.appendChild(this.triggerButton);\n }\n\n /**\n * Render modal using safe DOM methods (no innerHTML)\n */\n private renderModal(): void {\n if (!this.widgetConfig) return;\n\n const { config } = this.widgetConfig;\n\n // Overlay\n this.overlay = document.createElement('div');\n this.overlay.className = 'fv-widget-overlay';\n this.overlay.addEventListener('click', () => this.close());\n document.body.appendChild(this.overlay);\n\n // Modal container\n this.modal = document.createElement('div');\n this.modal.className = 'fv-widget-modal';\n\n // Header\n const header = document.createElement('div');\n header.className = 'fv-widget-header';\n\n const title = document.createElement('h2');\n title.className = 'fv-widget-title';\n title.textContent = config.formTitle; // Safe - textContent\n header.appendChild(title);\n\n const closeBtn = document.createElement('button');\n closeBtn.className = 'fv-widget-close';\n closeBtn.setAttribute('aria-label', 'Close');\n closeBtn.textContent = '×';\n closeBtn.addEventListener('click', () => this.close());\n header.appendChild(closeBtn);\n\n this.modal.appendChild(header);\n\n // Form\n const form = document.createElement('form');\n form.className = 'fv-widget-form';\n form.id = 'fv-feedback-form';\n\n const textarea = document.createElement('textarea');\n textarea.className = 'fv-widget-textarea';\n textarea.id = 'fv-feedback-content';\n textarea.placeholder = 'Tell us what you think...';\n textarea.required = true;\n form.appendChild(textarea);\n\n const submitBtn = document.createElement('button');\n submitBtn.type = 'submit';\n submitBtn.className = 'fv-widget-submit';\n submitBtn.textContent = config.submitButtonText; // Safe - textContent\n form.appendChild(submitBtn);\n\n const errorDiv = document.createElement('div');\n errorDiv.className = 'fv-widget-error';\n errorDiv.id = 'fv-error-message';\n form.appendChild(errorDiv);\n\n form.addEventListener('submit', (e) => this.handleFormSubmit(e));\n this.modal.appendChild(form);\n\n // Branding\n if (config.showBranding) {\n const branding = document.createElement('div');\n branding.className = 'fv-widget-branding';\n\n const brandText = document.createTextNode('Powered by ');\n branding.appendChild(brandText);\n\n const link = document.createElement('a');\n link.href = 'https://feedvalue.com';\n link.target = '_blank';\n link.rel = 'noopener noreferrer';\n link.textContent = 'FeedValue';\n branding.appendChild(link);\n\n this.modal.appendChild(branding);\n }\n\n document.body.appendChild(this.modal);\n }\n\n /**\n * Handle form submission\n */\n private async handleFormSubmit(event: Event): Promise<void> {\n event.preventDefault();\n\n const textarea = document.getElementById('fv-feedback-content') as HTMLTextAreaElement;\n const submitBtn = this.modal?.querySelector('.fv-widget-submit') as HTMLButtonElement;\n const errorEl = document.getElementById('fv-error-message');\n\n if (!textarea?.value.trim()) {\n this.showError('Please enter your feedback');\n return;\n }\n\n try {\n submitBtn.disabled = true;\n submitBtn.textContent = 'Submitting...';\n if (errorEl) errorEl.style.display = 'none';\n\n await this.submit({ message: textarea.value.trim() });\n\n // Show success\n this.showSuccess();\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Failed to submit';\n this.showError(message);\n } finally {\n submitBtn.disabled = false;\n submitBtn.textContent = this.widgetConfig?.config.submitButtonText ?? 'Submit';\n }\n }\n\n /**\n * Show error message (safe - uses textContent)\n */\n private showError(message: string): void {\n const errorEl = document.getElementById('fv-error-message');\n if (errorEl) {\n errorEl.textContent = message;\n errorEl.style.display = 'block';\n }\n }\n\n /**\n * Show success message using safe DOM methods\n */\n private showSuccess(): void {\n if (!this.modal || !this.widgetConfig) return;\n\n // Clear any existing auto-close timeout to prevent race conditions\n if (this.autoCloseTimeout) {\n clearTimeout(this.autoCloseTimeout);\n this.autoCloseTimeout = null;\n }\n\n // Clear modal\n this.modal.textContent = '';\n\n // Success container\n const successDiv = document.createElement('div');\n successDiv.style.cssText = 'text-align: center; padding: 40px 20px;';\n\n // Checkmark icon\n const iconDiv = document.createElement('div');\n iconDiv.style.cssText = 'font-size: 48px; margin-bottom: 16px;';\n iconDiv.textContent = '✓';\n successDiv.appendChild(iconDiv);\n\n // Thank you message\n const messageDiv = document.createElement('div');\n messageDiv.style.cssText = 'font-size: 16px; margin-bottom: 24px;';\n messageDiv.textContent = this.widgetConfig.config.thankYouMessage; // Safe\n successDiv.appendChild(messageDiv);\n\n // Close button\n const closeBtn = document.createElement('button');\n closeBtn.className = 'fv-widget-submit';\n closeBtn.textContent = 'Close';\n closeBtn.addEventListener('click', () => {\n this.close();\n this.resetForm();\n });\n successDiv.appendChild(closeBtn);\n\n this.modal.appendChild(successDiv);\n\n // Auto-close after delay (store reference for cleanup)\n this.autoCloseTimeout = setTimeout(() => {\n if (this.state.isOpen) {\n this.close();\n this.resetForm();\n }\n this.autoCloseTimeout = null;\n }, SUCCESS_AUTO_CLOSE_DELAY_MS);\n }\n\n /**\n * Reset form to initial state\n */\n private resetForm(): void {\n if (!this.modal) return;\n\n // Clear and rebuild modal\n this.modal.textContent = '';\n this.modal.remove();\n this.modal = null;\n\n // Re-render\n this.renderModal();\n }\n\n /**\n * Debug logging\n */\n private log(message: string, data?: unknown): void {\n if (this.config.debug) {\n console.log(`[FeedValue] ${message}`, data ?? '');\n }\n }\n}\n"],"mappings":"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","names":["index_exports","__export","ApiClient","DEFAULT_API_BASE_URL","FeedValue","TypedEventEmitter","clearFingerprint","generateFingerprint","TypedEventEmitter","__publicField","event","handler","wrappedHandler","args","handlers","error","DEFAULT_API_BASE_URL","ApiClient","baseUrl","DEFAULT_API_BASE_URL","debug","__publicField","widgetId","fingerprint","cacheKey","cached","pendingKey","pending","request","url","headers","response","error","data","feedback","userData","resetAt","retryAfter","errorData","errorMessage","retryResponse","message","FINGERPRINT_STORAGE_KEY","generateUUID","bytes","hex","b","c","r","generateFingerprint","stored","fingerprint","clearFingerprint","SUCCESS_AUTO_CLOSE_DELAY_MS","VALID_SENTIMENTS","MAX_MESSAGE_LENGTH","MAX_METADATA_VALUE_LENGTH","DEFAULT_CONFIG","instances","FeedValue","_FeedValue","options","__publicField","ApiClient","DEFAULT_API_BASE_URL","TypedEventEmitter","existing","instance","error","widgetId","fingerprint","generateFingerprint","configResponse","err","data","userId","traits","feedback","fullFeedback","userData","hasUserId","hasUserData","hasTraits","result","email","name","_e","_n","otherTraits","customData","filtered","key","value","event","callback","resolve","reject","config","partial","subscriber","css","BLOCKED_PATTERNS","pattern","styling","styleEl","sanitizedCSS","customStyleEl","position","positionStyles","modalPositionStyles","header","title","closeBtn","form","textarea","submitBtn","errorDiv","e","branding","brandText","link","errorEl","message","successDiv","iconDiv","messageDiv"]}
1
+ {"version":3,"sources":["../../src/index.ts","../../src/event-emitter.ts","../../src/api-client.ts","../../src/fingerprint.ts","../../src/feedvalue.ts"],"sourcesContent":["/**\n * @feedvalue/core\n *\n * Core FeedValue SDK for JavaScript/TypeScript applications.\n *\n * @example\n * ```ts\n * import { FeedValue } from '@feedvalue/core';\n *\n * // Initialize widget\n * const widget = FeedValue.init({ widgetId: 'your-widget-id' });\n *\n * // Control widget\n * widget.open();\n * widget.close();\n *\n * // Set user data\n * widget.setData({ email: 'user@example.com' });\n * widget.identify('user-123', { plan: 'pro' });\n *\n * // Listen to events\n * widget.on('submit', (feedback) => {\n * console.log('Feedback submitted:', feedback);\n * });\n *\n * // Programmatic submission\n * await widget.submit({ message: 'Great product!' });\n * ```\n *\n * @packageDocumentation\n */\n\n// Main class\nexport { FeedValue } from './feedvalue';\n\n// Types\nexport type {\n // Configuration\n FeedValueOptions,\n FeedValueConfig,\n WidgetConfig,\n WidgetPosition,\n WidgetTheme,\n WidgetUIConfig,\n WidgetStyling,\n TriggerIconType,\n\n // Custom Fields\n CustomField,\n CustomFieldType,\n EmojiSentiment,\n\n // State\n FeedValueState,\n\n // Feedback\n FeedbackData,\n FeedbackMetadata,\n\n // Events\n FeedValueEvents,\n EventHandler,\n\n // User Data\n UserData,\n UserTraits,\n\n // Instance Interface\n FeedValueInstance,\n\n // API Types (for advanced usage)\n ConfigResponse,\n FeedbackResponse,\n SubmissionUserData,\n} from './types';\n\n// Event Emitter (for advanced usage)\nexport { TypedEventEmitter } from './event-emitter';\n\n// API Client (for advanced usage)\nexport { ApiClient, DEFAULT_API_BASE_URL } from './api-client';\n\n// Fingerprint (for advanced usage)\nexport { generateFingerprint, clearFingerprint } from './fingerprint';\n","/**\n * @feedvalue/core - Type-Safe Event Emitter\n *\n * A minimal, type-safe event emitter for FeedValue events.\n * Provides compile-time type checking for event names and handlers.\n */\n\nimport type { FeedValueEvents, EventHandler } from './types';\n\n/**\n * Type-safe event emitter for FeedValue events\n *\n * @example\n * ```ts\n * const emitter = new TypedEventEmitter();\n *\n * emitter.on('ready', () => console.log('Ready!'));\n * emitter.on('submit', (feedback) => console.log('Submitted:', feedback));\n *\n * emitter.emit('ready');\n * emitter.emit('submit', { message: 'Great app!' });\n * ```\n */\nexport class TypedEventEmitter {\n // Using a Map with proper typing - the inner Function type is acceptable here\n // because we handle type safety at the public API level (on/off/emit methods)\n private listeners = new Map<keyof FeedValueEvents, Set<(...args: unknown[]) => void>>();\n\n /**\n * Subscribe to an event\n *\n * @param event - Event name\n * @param handler - Event handler function\n */\n on<K extends keyof FeedValueEvents>(event: K, handler: EventHandler<K>): void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n this.listeners.get(event)!.add(handler as (...args: unknown[]) => void);\n }\n\n /**\n * Subscribe to an event for a single emission.\n * The handler will be automatically removed after the first call.\n *\n * @param event - Event name\n * @param handler - Event handler function\n */\n once<K extends keyof FeedValueEvents>(event: K, handler: EventHandler<K>): void {\n const wrappedHandler = ((...args: unknown[]) => {\n this.off(event, wrappedHandler as EventHandler<K>);\n (handler as (...args: unknown[]) => void)(...args);\n }) as EventHandler<K>;\n\n this.on(event, wrappedHandler);\n }\n\n /**\n * Unsubscribe from an event\n *\n * @param event - Event name\n * @param handler - Optional handler to remove (removes all if not provided)\n */\n off<K extends keyof FeedValueEvents>(event: K, handler?: EventHandler<K>): void {\n if (!handler) {\n // Remove all handlers for this event\n this.listeners.delete(event);\n return;\n }\n\n const handlers = this.listeners.get(event);\n if (handlers) {\n handlers.delete(handler as (...args: unknown[]) => void);\n if (handlers.size === 0) {\n this.listeners.delete(event);\n }\n }\n }\n\n /**\n * Emit an event to all subscribers\n *\n * @param event - Event name\n * @param args - Arguments to pass to handlers\n */\n emit<K extends keyof FeedValueEvents>(\n event: K,\n ...args: Parameters<EventHandler<K>>\n ): void {\n const handlers = this.listeners.get(event);\n if (handlers) {\n for (const handler of handlers) {\n try {\n handler(...args);\n } catch (error) {\n // Prevent one handler's error from breaking others\n console.error(`[FeedValue] Error in ${event} handler:`, error);\n }\n }\n }\n }\n\n /**\n * Remove all event listeners\n */\n removeAllListeners(): void {\n this.listeners.clear();\n }\n}\n","/**\n * @feedvalue/core - API Client\n *\n * Handles all communication with the FeedValue API.\n * Includes request deduplication, caching, and error handling.\n */\n\nimport type { ConfigResponse, FeedbackResponse, FeedbackData, SubmissionUserData } from './types';\n\n/**\n * Default API base URL\n */\nexport const DEFAULT_API_BASE_URL = 'https://api.feedvalue.com';\n\n/** Buffer time before token is considered expired (seconds) */\nconst TOKEN_EXPIRY_BUFFER_SECONDS = 30;\n\n/** How long to cache widget config (milliseconds) */\nconst CONFIG_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes\n\n/**\n * Cache entry with TTL\n */\ninterface CacheEntry<T> {\n data: T;\n expiresAt: number;\n}\n\n/**\n * API client for FeedValue\n */\nexport class ApiClient {\n private baseUrl: string;\n private debug: boolean;\n\n // Request deduplication\n private pendingRequests = new Map<string, Promise<unknown>>();\n\n // Config cache\n private configCache = new Map<string, CacheEntry<ConfigResponse>>();\n\n // Anti-abuse tokens\n private submissionToken: string | null = null;\n private tokenExpiresAt: number | null = null;\n private fingerprint: string | null = null;\n\n constructor(baseUrl: string = DEFAULT_API_BASE_URL, debug = false) {\n this.baseUrl = baseUrl.replace(/\\/$/, ''); // Remove trailing slash\n this.debug = debug;\n }\n\n /**\n * Validate widget ID to prevent path injection attacks\n * @throws Error if widget ID is invalid\n */\n private validateWidgetId(widgetId: string): void {\n if (!widgetId || typeof widgetId !== 'string') {\n throw new Error('Widget ID is required');\n }\n if (!/^[a-zA-Z0-9_-]+$/.test(widgetId)) {\n throw new Error('Invalid widget ID format: only alphanumeric characters, underscores, and hyphens are allowed');\n }\n if (widgetId.length > 64) {\n throw new Error('Widget ID exceeds maximum length of 64 characters');\n }\n }\n\n /**\n * Set client fingerprint for anti-abuse protection\n */\n setFingerprint(fingerprint: string): void {\n this.fingerprint = fingerprint;\n }\n\n /**\n * Get client fingerprint\n */\n getFingerprint(): string | null {\n return this.fingerprint;\n }\n\n /**\n * Check if submission token is valid\n */\n hasValidToken(): boolean {\n if (!this.submissionToken || !this.tokenExpiresAt) {\n return false;\n }\n // Add buffer before expiry\n return Date.now() / 1000 < this.tokenExpiresAt - TOKEN_EXPIRY_BUFFER_SECONDS;\n }\n\n /**\n * Fetch widget configuration\n * Uses caching and request deduplication\n *\n * @param widgetId - Widget ID to fetch config for\n * @param forceRefresh - Skip cache and fetch fresh config (used for token refresh)\n */\n async fetchConfig(widgetId: string, forceRefresh = false): Promise<ConfigResponse> {\n this.validateWidgetId(widgetId);\n const cacheKey = `config:${widgetId}`;\n\n // Check cache first (unless forcing refresh)\n if (!forceRefresh) {\n const cached = this.configCache.get(cacheKey);\n if (cached && Date.now() < cached.expiresAt) {\n this.log('Config cache hit', { widgetId });\n return cached.data;\n }\n } else {\n this.log('Bypassing cache for token refresh', { widgetId });\n }\n\n // Deduplicate concurrent requests\n const pendingKey = `fetchConfig:${widgetId}`;\n const pending = this.pendingRequests.get(pendingKey);\n if (pending) {\n this.log('Deduplicating config request', { widgetId });\n return pending as Promise<ConfigResponse>;\n }\n\n const request = this.doFetchConfig(widgetId);\n this.pendingRequests.set(pendingKey, request);\n\n try {\n const result = await request;\n return result;\n } finally {\n this.pendingRequests.delete(pendingKey);\n }\n }\n\n /**\n * Actually fetch config from API\n */\n private async doFetchConfig(widgetId: string): Promise<ConfigResponse> {\n const url = `${this.baseUrl}/api/v1/widgets/${widgetId}/config`;\n\n const headers: Record<string, string> = {};\n if (this.fingerprint) {\n headers['X-Client-Fingerprint'] = this.fingerprint;\n }\n\n this.log('Fetching config', {\n widgetId,\n url,\n hasFingerprint: !!this.fingerprint,\n fingerprintPreview: this.fingerprint ? `${this.fingerprint.substring(0, 8)}...` : null,\n });\n\n const response = await fetch(url, {\n method: 'GET',\n headers,\n });\n\n if (!response.ok) {\n const error = await this.parseError(response);\n throw new Error(error);\n }\n\n const data = await response.json() as ConfigResponse;\n\n // Store submission token\n if (data.submission_token) {\n this.submissionToken = data.submission_token;\n this.tokenExpiresAt = data.token_expires_at ?? null;\n this.log('Submission token stored', {\n expiresAt: this.tokenExpiresAt ? new Date(this.tokenExpiresAt * 1000).toISOString() : 'unknown',\n });\n } else {\n this.log('No submission token in response', {\n hasWidgetId: !!data.widget_id,\n hasConfig: !!data.config,\n responseKeys: Object.keys(data),\n });\n }\n\n // Cache the response\n const cacheKey = `config:${widgetId}`;\n this.configCache.set(cacheKey, {\n data,\n expiresAt: Date.now() + CONFIG_CACHE_TTL_MS,\n });\n\n this.log('Config fetched', { widgetId });\n return data;\n }\n\n /**\n * Submit feedback with optional user data\n */\n async submitFeedback(\n widgetId: string,\n feedback: FeedbackData,\n userData?: SubmissionUserData\n ): Promise<FeedbackResponse> {\n this.validateWidgetId(widgetId);\n const url = `${this.baseUrl}/api/v1/widgets/${widgetId}/feedback`;\n\n // Refresh token if needed (force refresh to bypass cache)\n if (!this.hasValidToken()) {\n this.log('Token expired, refreshing...');\n await this.fetchConfig(widgetId, true);\n }\n\n if (!this.submissionToken) {\n throw new Error('No submission token available');\n }\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-Submission-Token': this.submissionToken,\n };\n\n if (this.fingerprint) {\n headers['X-Client-Fingerprint'] = this.fingerprint;\n }\n\n this.log('Submitting feedback', { widgetId });\n\n // Merge user data into metadata (core-api stores metadata, not separate user field)\n // User data from identify()/setData() goes into metadata.user\n const mergedMetadata = {\n ...feedback.metadata,\n ...(userData && Object.keys(userData).length > 0 && {\n user: userData,\n }),\n };\n\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n message: feedback.message,\n metadata: Object.keys(mergedMetadata).length > 0 ? mergedMetadata : undefined,\n ...(feedback.customFieldValues && {\n customFieldValues: feedback.customFieldValues,\n }),\n }),\n });\n\n // Handle rate limiting\n if (response.status === 429) {\n const resetAt = response.headers.get('X-RateLimit-Reset');\n const retryAfter = resetAt\n ? Math.ceil(parseInt(resetAt, 10) - Date.now() / 1000)\n : 60;\n throw new Error(`Rate limited. Try again in ${retryAfter} seconds.`);\n }\n\n // Handle forbidden (token issues)\n if (response.status === 403) {\n const errorData = await response.json().catch(() => ({ detail: 'Access denied' }));\n\n // Check for subscription limit\n if (errorData.detail?.code && errorData.detail?.message) {\n throw new Error(errorData.detail.message);\n }\n\n // Try token refresh and retry once\n const errorMessage = typeof errorData.detail === 'string' ? errorData.detail : '';\n if (errorMessage.includes('token') || errorMessage.includes('expired')) {\n this.log('Token rejected, refreshing...');\n this.submissionToken = null;\n await this.fetchConfig(widgetId, true);\n\n if (this.submissionToken) {\n // Retry with new token\n headers['X-Submission-Token'] = this.submissionToken;\n const retryResponse = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n message: feedback.message,\n metadata: feedback.metadata,\n ...(feedback.customFieldValues && {\n customFieldValues: feedback.customFieldValues,\n }),\n ...(userData && Object.keys(userData).length > 0 && {\n user: userData,\n }),\n }),\n });\n\n if (retryResponse.ok) {\n return retryResponse.json();\n }\n }\n }\n\n throw new Error(errorMessage || 'Access denied');\n }\n\n if (!response.ok) {\n const error = await this.parseError(response);\n throw new Error(error);\n }\n\n const data = await response.json() as FeedbackResponse;\n\n // Check for soft blocks\n if (data.blocked) {\n throw new Error(data.message || 'Unable to submit feedback');\n }\n\n this.log('Feedback submitted', { feedbackId: data.feedback_id });\n return data;\n }\n\n /**\n * Parse error from response\n */\n private async parseError(response: Response): Promise<string> {\n try {\n const data = await response.json();\n return data.detail || data.message || data.error || `HTTP ${response.status}`;\n } catch {\n return `HTTP ${response.status}: ${response.statusText}`;\n }\n }\n\n /**\n * Clear all caches\n */\n clearCache(): void {\n this.configCache.clear();\n this.submissionToken = null;\n this.tokenExpiresAt = null;\n this.log('Cache cleared');\n }\n\n /**\n * Debug logging\n */\n private log(message: string, data?: Record<string, unknown>): void {\n if (this.debug) {\n console.log(`[FeedValue API] ${message}`, data ?? '');\n }\n }\n}\n","/**\n * Simple fingerprint generation for anti-abuse protection.\n * Uses a session-based hex string stored in sessionStorage.\n *\n * This replaces the previous complex fingerprinting system (canvas, WebGL, audio)\n * which was overkill for an MVP feedback widget and raised privacy concerns.\n *\n * The fingerprint is a 32-character hex string (16 random bytes), matching\n * the format expected by the core-api's TokenPayload validator.\n */\n\n/**\n * Storage key for the fingerprint\n */\nconst FINGERPRINT_STORAGE_KEY = 'fv_fingerprint';\n\n/**\n * Generate a 32-character hex string (16 random bytes).\n * Uses crypto.getRandomValues which is available in all modern browsers and Node.js 15+.\n */\nfunction generateHexFingerprint(): string {\n if (typeof crypto === 'undefined' || typeof crypto.getRandomValues !== 'function') {\n throw new Error(\n 'crypto.getRandomValues is required but not available. ' +\n 'Ensure you are running in a modern browser or Node.js 15+.'\n );\n }\n\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Generate or retrieve a session fingerprint.\n *\n * The fingerprint is:\n * - Generated once per browser session as a 32-character hex string\n * - Stored in sessionStorage for consistency within a session\n * - Automatically cleared when the browser tab/window is closed\n *\n * @returns A unique fingerprint hex string for the current session\n */\nexport function generateFingerprint(): string {\n // Check for SSR environment\n if (typeof window === 'undefined' || typeof sessionStorage === 'undefined') {\n return generateHexFingerprint();\n }\n\n // Try to get existing fingerprint from session\n const stored = sessionStorage.getItem(FINGERPRINT_STORAGE_KEY);\n if (stored) {\n // Migrate old UUID format to hex format (remove dashes)\n if (stored.includes('-')) {\n const hexFingerprint = stored.replace(/-/g, '');\n try {\n sessionStorage.setItem(FINGERPRINT_STORAGE_KEY, hexFingerprint);\n } catch {\n // Ignore storage errors\n }\n return hexFingerprint;\n }\n return stored;\n }\n\n // Generate new fingerprint\n const fingerprint = generateHexFingerprint();\n\n try {\n sessionStorage.setItem(FINGERPRINT_STORAGE_KEY, fingerprint);\n } catch {\n // sessionStorage may be unavailable (private browsing, quota exceeded)\n // Fall through and return the generated fingerprint anyway\n }\n\n return fingerprint;\n}\n\n/**\n * Clear the stored fingerprint.\n * Useful for testing or when user requests data reset.\n */\nexport function clearFingerprint(): void {\n if (typeof sessionStorage !== 'undefined') {\n try {\n sessionStorage.removeItem(FINGERPRINT_STORAGE_KEY);\n } catch {\n // Ignore errors\n }\n }\n}\n","/**\n * @feedvalue/core - FeedValue Class\n *\n * Main FeedValue SDK class. Provides the public API for interacting\n * with the feedback widget.\n *\n * For vanilla JavaScript usage, this class also handles DOM rendering.\n * Framework packages (React, Vue) use this class as a headless core.\n */\n\nimport type {\n FeedValueOptions,\n FeedValueConfig,\n FeedValueState,\n FeedValueInstance,\n FeedValueEvents,\n EventHandler,\n UserData,\n UserTraits,\n FeedbackData,\n WidgetConfig,\n EmojiSentiment,\n SubmissionUserData,\n} from './types';\nimport { TypedEventEmitter } from './event-emitter';\nimport { ApiClient, DEFAULT_API_BASE_URL } from './api-client';\nimport { generateFingerprint } from './fingerprint';\n\n/** Delay before auto-closing the success message (milliseconds) */\nconst SUCCESS_AUTO_CLOSE_DELAY_MS = 3000;\n\n/**\n * Valid sentiment values for feedback validation\n */\nconst VALID_SENTIMENTS: readonly EmojiSentiment[] = ['angry', 'disappointed', 'satisfied', 'excited'] as const;\n\n/**\n * Maximum allowed message length (10,000 characters)\n */\nconst MAX_MESSAGE_LENGTH = 10000;\n\n/**\n * Maximum allowed length for metadata string values (1,000 characters)\n */\nconst MAX_METADATA_VALUE_LENGTH = 1000;\n\n/**\n * Default configuration\n */\nconst DEFAULT_CONFIG: FeedValueConfig = {\n theme: 'auto',\n autoShow: true,\n debug: false,\n locale: 'en',\n};\n\n/**\n * Instance registry for singleton per widgetId\n */\nconst instances = new Map<string, FeedValue>();\n\n/**\n * FeedValue SDK\n *\n * @example\n * ```ts\n * // Initialize\n * const feedvalue = FeedValue.init({ widgetId: 'abc123' });\n *\n * // Control\n * feedvalue.open();\n * feedvalue.close();\n *\n * // User data\n * feedvalue.setData({ email: 'user@example.com' });\n * feedvalue.identify('user-123', { plan: 'pro' });\n *\n * // Events\n * feedvalue.on('submit', (feedback) => {\n * console.log('Feedback submitted:', feedback);\n * });\n * ```\n */\nexport class FeedValue implements FeedValueInstance {\n private readonly widgetId: string;\n private readonly apiClient: ApiClient;\n private readonly emitter: TypedEventEmitter;\n private readonly headless: boolean;\n private config: FeedValueConfig;\n private widgetConfig: WidgetConfig | null = null;\n\n // State\n private state: FeedValueState = {\n isReady: false,\n isOpen: false,\n isVisible: true,\n error: null,\n isSubmitting: false,\n };\n\n // State subscribers (for React useSyncExternalStore)\n private stateSubscribers = new Set<() => void>();\n private stateSnapshot: FeedValueState;\n\n // User data (stored for future API submissions)\n private _userData: UserData = {};\n private _userId: string | null = null;\n private _userTraits: UserTraits = {};\n\n // DOM elements (for vanilla usage)\n private triggerButton: HTMLElement | null = null;\n private modal: HTMLElement | null = null;\n private overlay: HTMLElement | null = null;\n private stylesInjected = false;\n\n // Auto-close timeout reference (for cleanup on destroy)\n private autoCloseTimeout: ReturnType<typeof setTimeout> | null = null;\n\n /**\n * Create a new FeedValue instance\n * Use FeedValue.init() for public API\n */\n private constructor(options: FeedValueOptions) {\n this.widgetId = options.widgetId;\n this.headless = options.headless ?? false;\n this.config = { ...DEFAULT_CONFIG, ...options.config };\n\n this.apiClient = new ApiClient(\n options.apiBaseUrl ?? DEFAULT_API_BASE_URL,\n this.config.debug\n );\n\n this.emitter = new TypedEventEmitter();\n this.stateSnapshot = { ...this.state };\n\n this.log('Instance created', { widgetId: this.widgetId, headless: this.headless });\n }\n\n /**\n * Initialize FeedValue\n * Returns existing instance if already initialized for this widgetId\n */\n static init(options: FeedValueOptions): FeedValue {\n // Return existing instance if available\n const existing = instances.get(options.widgetId);\n if (existing) {\n return existing;\n }\n\n // Create new instance\n const instance = new FeedValue(options);\n instances.set(options.widgetId, instance);\n\n // Auto-initialize\n instance.init().catch((error) => {\n console.error('[FeedValue] Initialization failed:', error);\n });\n\n return instance;\n }\n\n /**\n * Get existing instance by widgetId\n */\n static getInstance(widgetId: string): FeedValue | undefined {\n return instances.get(widgetId);\n }\n\n // ===========================================================================\n // Lifecycle\n // ===========================================================================\n\n /**\n * Initialize the widget\n */\n async init(): Promise<void> {\n if (this.state.isReady) {\n this.log('Already initialized');\n return;\n }\n\n try {\n this.log('Initializing...');\n\n // Generate fingerprint\n const fingerprint = generateFingerprint();\n this.apiClient.setFingerprint(fingerprint);\n\n // Fetch config\n const configResponse = await this.apiClient.fetchConfig(this.widgetId);\n\n // Build widget config\n this.widgetConfig = {\n widgetId: configResponse.widget_id,\n widgetKey: configResponse.widget_key,\n appId: '',\n config: {\n position: configResponse.config.position ?? 'bottom-right',\n triggerText: configResponse.config.triggerText ?? 'Feedback',\n triggerIcon: configResponse.config.triggerIcon ?? 'none',\n formTitle: configResponse.config.formTitle ?? 'Share your feedback',\n submitButtonText: configResponse.config.submitButtonText ?? 'Submit',\n thankYouMessage: configResponse.config.thankYouMessage ?? 'Thank you for your feedback!',\n showBranding: configResponse.config.showBranding ?? true,\n customFields: configResponse.config.customFields,\n },\n styling: {\n primaryColor: configResponse.styling.primaryColor ?? '#3b82f6',\n backgroundColor: configResponse.styling.backgroundColor ?? '#ffffff',\n textColor: configResponse.styling.textColor ?? '#1f2937',\n buttonTextColor: configResponse.styling.buttonTextColor ?? '#ffffff',\n borderRadius: configResponse.styling.borderRadius ?? '8px',\n customCSS: configResponse.styling.customCSS,\n },\n };\n\n // Render DOM (for vanilla usage, skip in headless mode)\n if (!this.headless && typeof window !== 'undefined' && typeof document !== 'undefined') {\n this.renderWidget();\n }\n\n // Update state\n this.updateState({ isReady: true, error: null });\n this.emitter.emit('ready');\n\n this.log('Initialized successfully');\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n this.updateState({ error: err });\n this.emitter.emit('error', err);\n throw err;\n }\n }\n\n /**\n * Destroy the widget\n */\n destroy(): void {\n this.log('Destroying...');\n\n // Clear auto-close timeout to prevent memory leak\n if (this.autoCloseTimeout) {\n clearTimeout(this.autoCloseTimeout);\n this.autoCloseTimeout = null;\n }\n\n // Remove DOM elements\n this.triggerButton?.remove();\n this.modal?.remove();\n this.overlay?.remove();\n document.getElementById('fv-widget-styles')?.remove();\n document.getElementById('fv-widget-custom-styles')?.remove();\n\n // Clear references\n this.triggerButton = null;\n this.modal = null;\n this.overlay = null;\n this.widgetConfig = null;\n\n // Clear state\n this.stateSubscribers.clear();\n this.emitter.removeAllListeners();\n this.apiClient.clearCache();\n\n // Remove from registry\n instances.delete(this.widgetId);\n\n // Reset state\n this.state = {\n isReady: false,\n isOpen: false,\n isVisible: false,\n error: null,\n isSubmitting: false,\n };\n\n this.log('Destroyed');\n }\n\n // ===========================================================================\n // Widget Control\n // ===========================================================================\n\n open(): void {\n if (!this.state.isReady) {\n this.log('Cannot open: not ready');\n return;\n }\n\n this.updateState({ isOpen: true });\n\n // Only manipulate DOM in non-headless mode\n if (!this.headless) {\n this.overlay?.classList.add('fv-widget-open');\n this.modal?.classList.add('fv-widget-open');\n }\n\n this.emitter.emit('open');\n this.log('Opened');\n }\n\n close(): void {\n this.updateState({ isOpen: false });\n\n // Only manipulate DOM in non-headless mode\n if (!this.headless) {\n this.overlay?.classList.remove('fv-widget-open');\n this.modal?.classList.remove('fv-widget-open');\n }\n\n this.emitter.emit('close');\n this.log('Closed');\n }\n\n toggle(): void {\n if (this.state.isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n show(): void {\n this.updateState({ isVisible: true });\n\n // Only manipulate DOM in non-headless mode\n if (!this.headless && this.triggerButton) {\n this.triggerButton.style.display = '';\n }\n\n this.log('Shown');\n }\n\n hide(): void {\n this.updateState({ isVisible: false });\n\n // Only manipulate DOM in non-headless mode\n if (!this.headless && this.triggerButton) {\n this.triggerButton.style.display = 'none';\n }\n\n this.log('Hidden');\n }\n\n // ===========================================================================\n // State Queries\n // ===========================================================================\n\n isOpen(): boolean {\n return this.state.isOpen;\n }\n\n isVisible(): boolean {\n return this.state.isVisible;\n }\n\n isReady(): boolean {\n return this.state.isReady;\n }\n\n isHeadless(): boolean {\n return this.headless;\n }\n\n // ===========================================================================\n // User Data\n // ===========================================================================\n\n setData(data: Partial<UserData>): void {\n this._userData = { ...this._userData, ...data };\n this.log('User data set', data);\n }\n\n identify(userId: string, traits?: UserTraits): void {\n this._userId = userId;\n if (traits) {\n this._userTraits = { ...this._userTraits, ...traits };\n }\n this.log('User identified', { userId, traits });\n }\n\n reset(): void {\n this._userData = {};\n this._userId = null;\n this._userTraits = {};\n this.log('User data reset');\n }\n\n /**\n * Get current user data (for debugging/testing)\n */\n getUserData(): { userId: string | null; data: UserData; traits: UserTraits } {\n return {\n userId: this._userId,\n data: { ...this._userData },\n traits: { ...this._userTraits },\n };\n }\n\n // ===========================================================================\n // Feedback\n // ===========================================================================\n\n async submit(feedback: Partial<FeedbackData>): Promise<void> {\n if (!this.state.isReady) {\n throw new Error('Widget not ready');\n }\n\n // Validate feedback data before submission\n this.validateFeedback(feedback);\n\n this.updateState({ isSubmitting: true });\n\n try {\n const fullFeedback: FeedbackData = {\n message: feedback.message!,\n sentiment: feedback.sentiment,\n customFieldValues: feedback.customFieldValues,\n metadata: {\n page_url: typeof window !== 'undefined' ? window.location.href : '',\n referrer: typeof document !== 'undefined' ? document.referrer : undefined,\n user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,\n ...feedback.metadata,\n },\n };\n\n // Build user data for submission if any has been set\n const userData = this.buildSubmissionUserData();\n\n await this.apiClient.submitFeedback(this.widgetId, fullFeedback, userData);\n\n this.emitter.emit('submit', fullFeedback);\n this.log('Feedback submitted', userData ? { withUserData: true } : undefined);\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n this.emitter.emit('error', err);\n throw err;\n } finally {\n this.updateState({ isSubmitting: false });\n }\n }\n\n /**\n * Build user data object for API submission\n * Combines data from identify() and setData() calls\n */\n private buildSubmissionUserData(): SubmissionUserData | undefined {\n const hasUserId = this._userId !== null;\n const hasUserData = Object.keys(this._userData).length > 0;\n const hasTraits = Object.keys(this._userTraits).length > 0;\n\n // No user data set, return undefined\n if (!hasUserId && !hasUserData && !hasTraits) {\n return undefined;\n }\n\n const result: SubmissionUserData = {};\n\n // Add user ID if set via identify()\n if (this._userId) {\n result.user_id = this._userId;\n }\n\n // Extract email and name from userData (setData) or traits (identify)\n const email = this._userData.email ?? (this._userTraits.email as string | undefined);\n const name = this._userData.name ?? (this._userTraits.name as string | undefined);\n\n if (email) result.email = email;\n if (name) result.name = name;\n\n // Add traits (excluding email/name which are top-level)\n if (hasTraits) {\n const { email: _e, name: _n, ...otherTraits } = this._userTraits;\n if (Object.keys(otherTraits).length > 0) {\n result.traits = otherTraits;\n }\n }\n\n // Add custom data from setData() (excluding email/name which are top-level)\n if (hasUserData) {\n const { email: _e, name: _n, ...customData } = this._userData;\n // Filter out undefined values\n const filtered: Record<string, string> = {};\n for (const [key, value] of Object.entries(customData)) {\n if (value !== undefined) {\n filtered[key] = value;\n }\n }\n if (Object.keys(filtered).length > 0) {\n result.custom_data = filtered;\n }\n }\n\n return result;\n }\n\n /**\n * Validate feedback data before submission\n * @throws Error if validation fails\n */\n private validateFeedback(feedback: Partial<FeedbackData>): void {\n // Validate message is present and not empty\n if (!feedback.message?.trim()) {\n throw new Error('Feedback message is required');\n }\n\n // Validate message length\n if (feedback.message.length > MAX_MESSAGE_LENGTH) {\n throw new Error(`Feedback message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`);\n }\n\n // Validate sentiment if provided\n if (feedback.sentiment !== undefined && !VALID_SENTIMENTS.includes(feedback.sentiment)) {\n throw new Error(`Invalid sentiment value. Must be one of: ${VALID_SENTIMENTS.join(', ')}`);\n }\n\n // Validate customFieldValues are string key-value pairs\n if (feedback.customFieldValues) {\n for (const [key, value] of Object.entries(feedback.customFieldValues)) {\n if (typeof key !== 'string' || typeof value !== 'string') {\n throw new Error('Custom field values must be strings');\n }\n }\n }\n\n // Validate metadata field lengths\n if (feedback.metadata) {\n for (const [key, value] of Object.entries(feedback.metadata)) {\n if (typeof value === 'string' && value.length > MAX_METADATA_VALUE_LENGTH) {\n throw new Error(`Metadata field \"${key}\" exceeds maximum length of ${MAX_METADATA_VALUE_LENGTH} characters`);\n }\n }\n }\n }\n\n // ===========================================================================\n // Events\n // ===========================================================================\n\n on<K extends keyof FeedValueEvents>(event: K, callback: EventHandler<K>): void {\n this.emitter.on(event, callback);\n }\n\n /**\n * Subscribe to an event for a single emission.\n * The handler will be automatically removed after the first call.\n */\n once<K extends keyof FeedValueEvents>(event: K, callback: EventHandler<K>): void {\n this.emitter.once(event, callback);\n }\n\n off<K extends keyof FeedValueEvents>(event: K, callback?: EventHandler<K>): void {\n this.emitter.off(event, callback);\n }\n\n /**\n * Returns a promise that resolves when the widget is ready.\n * Useful for programmatic initialization flows.\n *\n * @throws {Error} If initialization fails\n */\n async waitUntilReady(): Promise<void> {\n if (this.state.isReady) {\n return;\n }\n\n if (this.state.error) {\n throw this.state.error;\n }\n\n return new Promise((resolve, reject) => {\n this.once('ready', () => resolve());\n this.once('error', (error) => reject(error));\n });\n }\n\n // ===========================================================================\n // Configuration\n // ===========================================================================\n\n setConfig(config: Partial<FeedValueConfig>): void {\n this.config = { ...this.config, ...config };\n this.log('Config updated', config);\n }\n\n getConfig(): FeedValueConfig {\n return { ...this.config };\n }\n\n /**\n * Get widget configuration (from API)\n */\n getWidgetConfig(): WidgetConfig | null {\n return this.widgetConfig;\n }\n\n // ===========================================================================\n // Framework Integration\n // ===========================================================================\n\n /**\n * Subscribe to state changes\n * Used by React's useSyncExternalStore\n */\n subscribe(callback: () => void): () => void {\n this.stateSubscribers.add(callback);\n return () => {\n this.stateSubscribers.delete(callback);\n };\n }\n\n /**\n * Get current state snapshot\n * Used by React's useSyncExternalStore\n */\n getSnapshot(): FeedValueState {\n return this.stateSnapshot;\n }\n\n // ===========================================================================\n // Internal Methods\n // ===========================================================================\n\n /**\n * Update state and notify subscribers\n */\n private updateState(partial: Partial<FeedValueState>): void {\n this.state = { ...this.state, ...partial };\n // Create new snapshot (important for React)\n this.stateSnapshot = { ...this.state };\n this.emitter.emit('stateChange', this.stateSnapshot);\n // Notify subscribers\n for (const subscriber of this.stateSubscribers) {\n subscriber();\n }\n }\n\n /**\n * Render widget DOM elements (for vanilla usage)\n */\n private renderWidget(): void {\n if (!this.widgetConfig) return;\n\n // Inject styles once\n if (!this.stylesInjected) {\n this.injectStyles();\n this.stylesInjected = true;\n }\n\n // Create trigger button\n this.renderTrigger();\n\n // Create modal\n this.renderModal();\n }\n\n /**\n * Sanitize CSS to block potentially dangerous patterns\n * Prevents CSS injection attacks via url(), @import, and other vectors\n */\n private sanitizeCSS(css: string): string {\n // Block potentially dangerous CSS patterns\n const BLOCKED_PATTERNS = [\n /url\\s*\\(/gi, // External resources\n /@import/gi, // External stylesheets\n /expression\\s*\\(/gi, // IE expressions\n /javascript:/gi, // JavaScript URLs\n /behavior\\s*:/gi, // IE behaviors\n /-moz-binding/gi, // Firefox XBL\n ];\n\n for (const pattern of BLOCKED_PATTERNS) {\n if (pattern.test(css)) {\n console.warn('[FeedValue] Blocked potentially unsafe CSS pattern');\n return ''; // Block entire CSS if unsafe pattern found\n }\n }\n return css;\n }\n\n /**\n * Inject CSS styles\n */\n private injectStyles(): void {\n if (!this.widgetConfig) return;\n\n const { styling, config } = this.widgetConfig;\n\n const styleEl = document.createElement('style');\n styleEl.id = 'fv-widget-styles';\n styleEl.textContent = this.getBaseStyles(styling, config.position);\n document.head.appendChild(styleEl);\n\n // Custom CSS - sanitize to prevent CSS injection attacks\n if (styling.customCSS) {\n const sanitizedCSS = this.sanitizeCSS(styling.customCSS);\n if (sanitizedCSS) {\n const customStyleEl = document.createElement('style');\n customStyleEl.id = 'fv-widget-custom-styles';\n customStyleEl.textContent = sanitizedCSS;\n document.head.appendChild(customStyleEl);\n }\n }\n }\n\n /**\n * Get base CSS styles\n */\n private getBaseStyles(styling: WidgetConfig['styling'], position: string): string {\n const positionStyles = this.getPositionStyles(position);\n const modalPositionStyles = this.getModalPositionStyles(position);\n\n return `\n .fv-widget-trigger {\n position: fixed;\n ${positionStyles}\n background-color: ${styling.primaryColor};\n color: ${styling.buttonTextColor};\n padding: 12px 24px;\n border-radius: ${styling.borderRadius};\n cursor: pointer;\n z-index: 9998;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 14px;\n font-weight: 500;\n border: none;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n transition: transform 0.2s, box-shadow 0.2s;\n }\n .fv-widget-trigger:hover {\n transform: translateY(-2px);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);\n }\n .fv-widget-overlay {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgba(0, 0, 0, 0.5);\n z-index: 9998;\n display: none;\n backdrop-filter: blur(4px);\n }\n .fv-widget-overlay.fv-widget-open {\n display: block;\n }\n .fv-widget-modal {\n position: fixed;\n ${modalPositionStyles}\n background-color: ${styling.backgroundColor};\n color: ${styling.textColor};\n border-radius: ${styling.borderRadius};\n padding: 24px;\n max-width: 500px;\n width: 90%;\n max-height: 90vh;\n overflow-y: auto;\n z-index: 9999;\n display: none;\n box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);\n font-family: system-ui, -apple-system, sans-serif;\n }\n .fv-widget-modal.fv-widget-open {\n display: block;\n }\n .fv-widget-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 20px;\n }\n .fv-widget-title {\n font-size: 20px;\n font-weight: 600;\n margin: 0;\n }\n .fv-widget-close {\n background: transparent;\n border: none;\n font-size: 24px;\n cursor: pointer;\n color: ${styling.textColor};\n padding: 0;\n width: 32px;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .fv-widget-form {\n display: flex;\n flex-direction: column;\n gap: 16px;\n }\n .fv-widget-textarea {\n width: 100%;\n min-height: 120px;\n padding: 12px;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: ${styling.borderRadius};\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 14px;\n resize: vertical;\n box-sizing: border-box;\n background-color: ${styling.backgroundColor};\n color: ${styling.textColor};\n }\n .fv-widget-textarea:focus {\n outline: none;\n border-color: ${styling.primaryColor};\n box-shadow: 0 0 0 2px ${styling.primaryColor}33;\n }\n .fv-widget-submit {\n background-color: ${styling.primaryColor};\n color: ${styling.buttonTextColor};\n padding: 12px 24px;\n border: none;\n border-radius: ${styling.borderRadius};\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: opacity 0.2s;\n }\n .fv-widget-submit:hover:not(:disabled) {\n opacity: 0.9;\n }\n .fv-widget-submit:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n .fv-widget-error {\n color: #ef4444;\n font-size: 14px;\n margin-top: 8px;\n display: none;\n }\n .fv-widget-branding {\n text-align: center;\n font-size: 12px;\n color: ${styling.textColor};\n margin-top: 16px;\n opacity: 0.7;\n }\n .fv-widget-branding a {\n color: ${styling.primaryColor};\n text-decoration: none;\n }\n `;\n }\n\n /**\n * Get trigger button position styles\n */\n private getPositionStyles(position: string): string {\n switch (position) {\n case 'bottom-left':\n return 'bottom: 20px; left: 20px;';\n case 'top-right':\n return 'top: 20px; right: 20px;';\n case 'top-left':\n return 'top: 20px; left: 20px;';\n case 'center':\n return 'top: 50%; left: 50%; transform: translate(-50%, -50%);';\n case 'bottom-right':\n default:\n return 'bottom: 20px; right: 20px;';\n }\n }\n\n /**\n * Get modal position styles\n */\n private getModalPositionStyles(position: string): string {\n switch (position) {\n case 'bottom-left':\n return 'bottom: 20px; left: 20px;';\n case 'bottom-right':\n return 'bottom: 20px; right: 20px;';\n case 'top-right':\n return 'top: 20px; right: 20px;';\n case 'top-left':\n return 'top: 20px; left: 20px;';\n case 'center':\n default:\n return 'top: 50%; left: 50%; transform: translate(-50%, -50%);';\n }\n }\n\n /**\n * Render trigger button using safe DOM methods\n */\n private renderTrigger(): void {\n if (!this.widgetConfig) return;\n\n this.triggerButton = document.createElement('button');\n this.triggerButton.className = 'fv-widget-trigger';\n // textContent is safe - no HTML parsing\n this.triggerButton.textContent = this.widgetConfig.config.triggerText;\n this.triggerButton.addEventListener('click', () => this.open());\n\n document.body.appendChild(this.triggerButton);\n }\n\n /**\n * Render modal using safe DOM methods (no innerHTML)\n */\n private renderModal(): void {\n if (!this.widgetConfig) return;\n\n const { config } = this.widgetConfig;\n\n // Overlay\n this.overlay = document.createElement('div');\n this.overlay.className = 'fv-widget-overlay';\n this.overlay.addEventListener('click', () => this.close());\n document.body.appendChild(this.overlay);\n\n // Modal container\n this.modal = document.createElement('div');\n this.modal.className = 'fv-widget-modal';\n\n // Header\n const header = document.createElement('div');\n header.className = 'fv-widget-header';\n\n const title = document.createElement('h2');\n title.className = 'fv-widget-title';\n title.textContent = config.formTitle; // Safe - textContent\n header.appendChild(title);\n\n const closeBtn = document.createElement('button');\n closeBtn.className = 'fv-widget-close';\n closeBtn.setAttribute('aria-label', 'Close');\n closeBtn.textContent = '×';\n closeBtn.addEventListener('click', () => this.close());\n header.appendChild(closeBtn);\n\n this.modal.appendChild(header);\n\n // Form\n const form = document.createElement('form');\n form.className = 'fv-widget-form';\n form.id = 'fv-feedback-form';\n\n const textarea = document.createElement('textarea');\n textarea.className = 'fv-widget-textarea';\n textarea.id = 'fv-feedback-content';\n textarea.placeholder = 'Tell us what you think...';\n textarea.required = true;\n form.appendChild(textarea);\n\n const submitBtn = document.createElement('button');\n submitBtn.type = 'submit';\n submitBtn.className = 'fv-widget-submit';\n submitBtn.textContent = config.submitButtonText; // Safe - textContent\n form.appendChild(submitBtn);\n\n const errorDiv = document.createElement('div');\n errorDiv.className = 'fv-widget-error';\n errorDiv.id = 'fv-error-message';\n form.appendChild(errorDiv);\n\n form.addEventListener('submit', (e) => this.handleFormSubmit(e));\n this.modal.appendChild(form);\n\n // Branding\n if (config.showBranding) {\n const branding = document.createElement('div');\n branding.className = 'fv-widget-branding';\n\n const brandText = document.createTextNode('Powered by ');\n branding.appendChild(brandText);\n\n const link = document.createElement('a');\n link.href = 'https://feedvalue.com';\n link.target = '_blank';\n link.rel = 'noopener noreferrer';\n link.textContent = 'FeedValue';\n branding.appendChild(link);\n\n this.modal.appendChild(branding);\n }\n\n document.body.appendChild(this.modal);\n }\n\n /**\n * Handle form submission\n */\n private async handleFormSubmit(event: Event): Promise<void> {\n event.preventDefault();\n\n const textarea = document.getElementById('fv-feedback-content') as HTMLTextAreaElement;\n const submitBtn = this.modal?.querySelector('.fv-widget-submit') as HTMLButtonElement;\n const errorEl = document.getElementById('fv-error-message');\n\n if (!textarea?.value.trim()) {\n this.showError('Please enter your feedback');\n return;\n }\n\n try {\n submitBtn.disabled = true;\n submitBtn.textContent = 'Submitting...';\n if (errorEl) errorEl.style.display = 'none';\n\n await this.submit({ message: textarea.value.trim() });\n\n // Show success\n this.showSuccess();\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Failed to submit';\n this.showError(message);\n } finally {\n submitBtn.disabled = false;\n submitBtn.textContent = this.widgetConfig?.config.submitButtonText ?? 'Submit';\n }\n }\n\n /**\n * Show error message (safe - uses textContent)\n */\n private showError(message: string): void {\n const errorEl = document.getElementById('fv-error-message');\n if (errorEl) {\n errorEl.textContent = message;\n errorEl.style.display = 'block';\n }\n }\n\n /**\n * Show success message using safe DOM methods\n */\n private showSuccess(): void {\n if (!this.modal || !this.widgetConfig) return;\n\n // Clear any existing auto-close timeout to prevent race conditions\n if (this.autoCloseTimeout) {\n clearTimeout(this.autoCloseTimeout);\n this.autoCloseTimeout = null;\n }\n\n // Clear modal\n this.modal.textContent = '';\n\n // Success container\n const successDiv = document.createElement('div');\n successDiv.style.cssText = 'text-align: center; padding: 40px 20px;';\n\n // Checkmark icon\n const iconDiv = document.createElement('div');\n iconDiv.style.cssText = 'font-size: 48px; margin-bottom: 16px;';\n iconDiv.textContent = '✓';\n successDiv.appendChild(iconDiv);\n\n // Thank you message\n const messageDiv = document.createElement('div');\n messageDiv.style.cssText = 'font-size: 16px; margin-bottom: 24px;';\n messageDiv.textContent = this.widgetConfig.config.thankYouMessage; // Safe\n successDiv.appendChild(messageDiv);\n\n // Close button\n const closeBtn = document.createElement('button');\n closeBtn.className = 'fv-widget-submit';\n closeBtn.textContent = 'Close';\n closeBtn.addEventListener('click', () => {\n this.close();\n this.resetForm();\n });\n successDiv.appendChild(closeBtn);\n\n this.modal.appendChild(successDiv);\n\n // Auto-close after delay (store reference for cleanup)\n this.autoCloseTimeout = setTimeout(() => {\n if (this.state.isOpen) {\n this.close();\n this.resetForm();\n }\n this.autoCloseTimeout = null;\n }, SUCCESS_AUTO_CLOSE_DELAY_MS);\n }\n\n /**\n * Reset form to initial state\n */\n private resetForm(): void {\n if (!this.modal) return;\n\n // Clear and rebuild modal\n this.modal.textContent = '';\n this.modal.remove();\n this.modal = null;\n\n // Re-render\n this.renderModal();\n }\n\n /**\n * Debug logging\n */\n private log(message: string, data?: unknown): void {\n if (this.config.debug) {\n console.log(`[FeedValue] ${message}`, data ?? '');\n }\n }\n}\n"],"mappings":"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","names":["index_exports","__export","ApiClient","DEFAULT_API_BASE_URL","FeedValue","TypedEventEmitter","clearFingerprint","generateFingerprint","TypedEventEmitter","__publicField","event","handler","wrappedHandler","args","handlers","error","DEFAULT_API_BASE_URL","ApiClient","baseUrl","DEFAULT_API_BASE_URL","debug","__publicField","widgetId","fingerprint","forceRefresh","cacheKey","cached","pendingKey","pending","request","url","headers","response","error","data","feedback","userData","mergedMetadata","resetAt","retryAfter","errorData","errorMessage","retryResponse","message","FINGERPRINT_STORAGE_KEY","generateHexFingerprint","bytes","b","generateFingerprint","stored","hexFingerprint","fingerprint","clearFingerprint","SUCCESS_AUTO_CLOSE_DELAY_MS","VALID_SENTIMENTS","MAX_MESSAGE_LENGTH","MAX_METADATA_VALUE_LENGTH","DEFAULT_CONFIG","instances","FeedValue","_FeedValue","options","__publicField","ApiClient","DEFAULT_API_BASE_URL","TypedEventEmitter","existing","instance","error","widgetId","fingerprint","generateFingerprint","configResponse","err","data","userId","traits","feedback","fullFeedback","userData","hasUserId","hasUserData","hasTraits","result","email","name","_e","_n","otherTraits","customData","filtered","key","value","event","callback","resolve","reject","config","partial","subscriber","css","BLOCKED_PATTERNS","pattern","styling","styleEl","sanitizedCSS","customStyleEl","position","positionStyles","modalPositionStyles","header","title","closeBtn","form","textarea","submitBtn","errorDiv","e","branding","brandText","link","errorEl","message","successDiv","iconDiv","messageDiv"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feedvalue/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "FeedValue Core SDK - TypeScript types, initialization, and vanilla JavaScript API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -23,10 +23,19 @@
23
23
  "README.md"
24
24
  ],
25
25
  "sideEffects": false,
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "dev": "tsup --watch",
29
+ "lint": "eslint src",
30
+ "test": "vitest run",
31
+ "test:watch": "vitest",
32
+ "test:coverage": "vitest run --coverage",
33
+ "typecheck": "tsc --noEmit",
34
+ "clean": "rm -rf dist"
35
+ },
26
36
  "devDependencies": {
27
37
  "@types/node": "^22.10.0",
28
38
  "@vitest/coverage-v8": "^2.1.0",
29
- "eslint": "^9.17.0",
30
39
  "happy-dom": "^15.11.0",
31
40
  "tsup": "^8.3.0",
32
41
  "typescript": "^5.7.0",
@@ -56,15 +65,5 @@
56
65
  ],
57
66
  "engines": {
58
67
  "node": ">=18"
59
- },
60
- "scripts": {
61
- "build": "tsup",
62
- "dev": "tsup --watch",
63
- "lint": "eslint src --ext .ts",
64
- "test": "vitest run",
65
- "test:watch": "vitest",
66
- "test:coverage": "vitest run --coverage",
67
- "typecheck": "tsc --noEmit",
68
- "clean": "rm -rf dist"
69
68
  }
70
- }
69
+ }