@saas-support/react 0.3.1 → 0.4.0

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.
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class b extends Error{constructor(t,e,s="unknown"){super(e),this.name="SaaSError",this.code=t,this.domain=s}get isNotFound(){return this.code===404}get isUnauthorized(){return this.code===401}get isForbidden(){return this.code===403}get isConflict(){return this.code===409}get isRateLimited(){return this.code===429}}class g{constructor(t,e){this.baseUrl=t,this.authMode=e}async request(t,e,s,r){const i={"Content-Type":"application/json",...this.getAuthHeaders(),...r},a=await(await fetch(`${this.baseUrl}${e}`,{method:t,headers:i,body:s?JSON.stringify(s):void 0})).json();if(a.code&&a.code>=400){const d=this.inferDomain(e);throw new b(a.code,a.message||"Request failed",d)}return a.data}async get(t,e){return this.request("GET",t,void 0,e)}async post(t,e,s){return this.request("POST",t,e,s)}async patch(t,e,s){return this.request("PATCH",t,e,s)}async del(t,e){return this.request("DELETE",t,void 0,e)}getAuthHeaders(){switch(this.authMode.type){case"publishableKey":case"apiKey":return{"X-API-Key":this.authMode.key};case"portalToken":case"embedToken":return{Authorization:`Bearer ${this.authMode.token}`}}}inferDomain(t){return t.startsWith("/auth")?"auth":t.startsWith("/billing")?"billing":t.startsWith("/report")?"report":"unknown"}}class R{constructor(t){this.accessToken=null,this.refreshToken=null,this.refreshTimer=null,this.onRefreshNeeded=null,this.storageKey=`ss_rt_${t.slice(0,12)}`,this.refreshToken=this.loadRefreshToken()}setRefreshCallback(t){this.onRefreshNeeded=t}getAccessToken(){return this.accessToken}getRefreshToken(){return this.refreshToken}hasRefreshToken(){return this.refreshToken!==null}setTokens(t,e){this.accessToken=t,this.refreshToken=e,this.saveRefreshToken(e),this.scheduleRefresh(t)}clearTokens(){this.accessToken=null,this.refreshToken=null,this.removeRefreshToken(),this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=null)}destroy(){this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=null)}scheduleRefresh(t){var r;this.refreshTimer&&clearTimeout(this.refreshTimer);const e=this.getTokenExpiry(t);if(!e)return;const s=e*1e3-Date.now()-6e4;if(s<=0){(r=this.onRefreshNeeded)==null||r.call(this);return}this.refreshTimer=setTimeout(()=>{var i;(i=this.onRefreshNeeded)==null||i.call(this)},s)}getTokenExpiry(t){try{const e=t.split(".")[1];return JSON.parse(atob(e)).exp??null}catch{return null}}loadRefreshToken(){try{return localStorage.getItem(this.storageKey)}catch{return null}}saveRefreshToken(t){try{localStorage.setItem(this.storageKey,t)}catch{}}removeRefreshToken(){try{localStorage.removeItem(this.storageKey)}catch{}}}class M{constructor(){this.listeners=new Map}on(t,e){return this.listeners.has(t)||this.listeners.set(t,new Set),this.listeners.get(t).add(e),()=>{var s;(s=this.listeners.get(t))==null||s.delete(e)}}emit(t,e){var s;(s=this.listeners.get(t))==null||s.forEach(r=>r(e))}removeAll(){this.listeners.clear()}}const k=500,T=600,U=5*60*1e3;class w{constructor(t,e,s,r){this.cachedUser=null,this.cachedSettings=null,this.loaded=!1,this.transport=t,this.tokenManager=e,this.emitter=s,this.baseUrl=r}async load(){var t,e;if(!this.loaded){try{this.cachedSettings=await this.transport.get("/auth/settings")}catch(s){console.warn("[SaaS Support] Failed to load project settings:",s)}if((t=this.tokenManager)!=null&&t.hasRefreshToken())try{await this.performRefresh()}catch{(e=this.tokenManager)==null||e.clearTokens()}this.loaded=!0}}async signIn(t,e){const s=await this.transport.post("/auth/login",{email:t,password:e});if("mfaRequired"in s&&s.mfaRequired)return s;const r=s;return this.setSession(r),r}async signUp(t,e){const s=await this.transport.post("/auth/register",{email:t,password:e});return this.setSession(s),s}async signOut(){var e;const t=(e=this.tokenManager)==null?void 0:e.getRefreshToken();if(t)try{await this.transport.post("/auth/logout",{refreshToken:t})}catch{}this.clearSession()}async signInWithOAuth(t){const e=`${this.baseUrl}/auth/oauth/${t}/popup-callback`,{authUrl:s,state:r}=await this.transport.get(`/auth/oauth/${t}?redirect_uri=${encodeURIComponent(e)}`),i=window.screenX+(window.innerWidth-k)/2,y=window.screenY+(window.innerHeight-T)/2,a=window.open(s,"saas-support-oauth",`width=${k},height=${T},left=${i},top=${y},toolbar=no,menubar=no`);return new Promise((d,c)=>{let o=!1;const u=async h=>{var m;if(((m=h.data)==null?void 0:m.type)==="saas-support:oauth-callback"&&!o){if(o=!0,window.removeEventListener("message",u),clearTimeout(f),clearInterval(p),a==null||a.close(),h.data.error){c(new Error(`OAuth error: ${h.data.error}`));return}try{const l=await this.transport.post(`/auth/oauth/${t}/callback`,{code:h.data.code,state:h.data.state||r});this.setSession(l),d(l)}catch(l){c(l)}}};window.addEventListener("message",u);const f=setTimeout(()=>{o||(o=!0,window.removeEventListener("message",u),clearInterval(p),a==null||a.close(),c(new Error("OAuth popup timed out")))},U),p=setInterval(()=>{a!=null&&a.closed&&!o&&(o=!0,clearInterval(p),clearTimeout(f),window.removeEventListener("message",u),c(new Error("OAuth popup was closed")))},500)})}async submitMfaCode(t,e){const s=await this.transport.post("/auth/login/mfa",{mfaToken:t,code:e});return this.setSession(s),s}async sendMagicLink(t,e){await this.transport.post("/auth/magic-link/send",{email:t,redirectUrl:e})}async verifyMagicLink(t){const e=await this.transport.post("/auth/magic-link/verify",{token:t});return this.setSession(e),e}async sendPasswordReset(t,e){await this.transport.post("/auth/password-reset/send",{email:t,redirectUrl:e})}async resetPassword(t,e){await this.transport.post("/auth/password-reset/verify",{token:t,newPassword:e})}async setupMfa(){return this.transport.post("/auth/mfa/setup",void 0,this.authHeaders())}async verifyMfa(t){return this.transport.post("/auth/mfa/verify",{code:t},this.authHeaders())}async disableMfa(t){await this.transport.post("/auth/mfa/disable",{code:t},this.authHeaders())}async getToken(){var e,s,r;const t=((e=this.tokenManager)==null?void 0:e.getAccessToken())??null;if(t)return t;if((s=this.tokenManager)!=null&&s.hasRefreshToken())try{return await this.performRefresh(),((r=this.tokenManager)==null?void 0:r.getAccessToken())??null}catch{return this.clearSession(),null}return null}async getUser(){if(this.cachedUser)return this.cachedUser;const t=await this.getToken();if(!t)return null;try{return this.cachedUser=await this.transport.get("/auth/me",{Authorization:`Bearer ${t}`}),this.cachedUser}catch{return null}}getUserSync(){return this.cachedUser}isLoaded(){return this.loaded}async getSettings(){if(this.cachedSettings)return this.cachedSettings;try{return this.cachedSettings=await this.transport.get("/auth/settings"),this.cachedSettings}catch{return null}}onAuthStateChange(t){return this.emitter.on("authStateChange",t)}async updateProfile(t){const e=await this.transport.patch("/auth/me",t,this.authHeaders());return this.cachedUser=e,this.emitter.emit("authStateChange",e),e}async changePassword(t,e){await this.transport.post("/auth/change-password",{currentPassword:t,newPassword:e},this.authHeaders())}async listOrgs(){return this.transport.get("/auth/orgs",this.authHeaders())}async createOrg(t,e){return this.transport.post("/auth/orgs",{name:t,slug:e},this.authHeaders())}async getOrg(t){return this.transport.get(`/auth/orgs/${t}`,this.authHeaders())}async updateOrg(t,e){return this.transport.patch(`/auth/orgs/${t}`,e,this.authHeaders())}async deleteOrg(t){await this.transport.del(`/auth/orgs/${t}`,this.authHeaders())}async listMembers(t){return this.transport.get(`/auth/orgs/${t}/members`,this.authHeaders())}async sendInvite(t,e,s){return this.transport.post(`/auth/orgs/${t}/invites`,{email:e,role:s},this.authHeaders())}async updateMemberRole(t,e,s){await this.transport.patch(`/auth/orgs/${t}/members/${e}`,{role:s},this.authHeaders())}async removeMember(t,e){await this.transport.del(`/auth/orgs/${t}/members/${e}`,this.authHeaders())}async acceptInvite(t){return this.transport.post(`/auth/invites/${t}/accept`,void 0,this.authHeaders())}async performRefresh(){var s;const t=(s=this.tokenManager)==null?void 0:s.getRefreshToken();if(!t)throw new Error("No refresh token");const e=await this.transport.post("/auth/refresh",{refreshToken:t});if(this.tokenManager.setTokens(e.accessToken,e.refreshToken),!this.cachedUser)try{this.cachedUser=await this.transport.get("/auth/me",{Authorization:`Bearer ${e.accessToken}`}),this.emitter.emit("authStateChange",this.cachedUser)}catch{}}setSession(t){var e;(e=this.tokenManager)==null||e.setTokens(t.accessToken,t.refreshToken),this.cachedUser=t.user,this.emitter.emit("authStateChange",t.user)}clearSession(){var t;(t=this.tokenManager)==null||t.clearTokens(),this.cachedUser=null,this.emitter.emit("authStateChange",null)}authHeaders(){var e;const t=(e=this.tokenManager)==null?void 0:e.getAccessToken();return t?{Authorization:`Bearer ${t}`}:{}}}class S{constructor(t){this.transport=t}async createCustomer(t){return this.transport.post("/billing/customers",t)}async getCustomer(t){return this.transport.get(`/billing/customers/${t}`)}async updateCustomer(t,e){return this.transport.patch(`/billing/customers/${t}`,e)}async subscribe(t,e){return this.transport.post(`/billing/customers/${t}/subscribe`,{planId:e})}async changePlan(t,e){return this.transport.patch(`/billing/customers/${t}/subscription`,{planId:e})}async cancelSubscription(t){return this.transport.del(`/billing/customers/${t}/subscription`)}async getInvoices(t){return this.transport.get(`/billing/customers/${t}/invoices`)}async ingestUsageEvent(t){return this.transport.post("/billing/events",t)}async getCurrentUsage(t){return this.transport.get(`/billing/customers/${t}/usage`)}async createPortalToken(t,e){return this.transport.post("/billing/portal-tokens",{customerId:t,expiresIn:e})}async applyCoupon(t,e){return this.transport.post(`/billing/customers/${t}/coupon`,{code:e})}}class ${constructor(t){this.transport=t}async executeQuery(t){return this.transport.post("/report/query",t)}async listQueries(t){const e=t?this.toQueryString(t):"";return this.transport.get(`/report/queries${e}`)}async saveQuery(t){return this.transport.post("/report/queries",t)}async updateQuery(t,e){return this.transport.patch(`/report/queries/${t}`,e)}async deleteQuery(t){await this.transport.del(`/report/queries/${t}`)}async listDashboards(t){const e=t?this.toQueryString(t):"";return this.transport.get(`/report/dashboards${e}`)}async createDashboard(t){return this.transport.post("/report/dashboards",t)}async getDashboard(t){return this.transport.get(`/report/dashboards/${t}`)}async updateDashboard(t,e){return this.transport.patch(`/report/dashboards/${t}`,e)}async deleteDashboard(t){await this.transport.del(`/report/dashboards/${t}`)}async createEmbedToken(t){return this.transport.post("/report/embed-tokens",t)}async listEmbedTokens(){return this.transport.get("/report/embed-tokens")}async revokeEmbedToken(t){await this.transport.del(`/report/embed-tokens/${t}`)}toQueryString(t){const e=Object.entries(t).filter(([,s])=>s!=null&&s!=="");return e.length===0?"":"?"+e.map(([s,r])=>`${s}=${encodeURIComponent(String(r))}`).join("&")}}const v="https://api.saas-support.com/v1";class E{constructor(t){if(this.tokenManager=null,this.loaded=!1,!t.publishableKey&&!t.apiKey)throw new Error("SaaSSupport: either publishableKey or apiKey is required");const e=t.baseUrl??v;this.emitter=new M;const s=t.publishableKey?new g(e,{type:"publishableKey",key:t.publishableKey}):null,r=t.apiKey?new g(e,{type:"apiKey",key:t.apiKey}):null;t.publishableKey&&(this.tokenManager=new R(t.publishableKey)),this.auth=new w(s??r,this.tokenManager,this.emitter,e),this.billing=new S(r??s),this.report=new $(r??s),this.tokenManager&&this.tokenManager.setRefreshCallback(()=>this.auth.performRefresh())}async load(){this.loaded||(await this.auth.load(),this.loaded=!0)}isLoaded(){return this.loaded}onError(t){return this.emitter.on("error",t)}destroy(){var t;(t=this.tokenManager)==null||t.destroy(),this.emitter.removeAll()}}function C(n){return"mfaRequired"in n&&n.mfaRequired===!0}exports.AuthClient=w;exports.BillingClient=S;exports.ReportClient=$;exports.SaaSError=b;exports.SaaSSupport=E;exports.Transport=g;exports.isMfaRequired=C;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class c extends Error{constructor(e,t,s="unknown"){super(t),this.name="SaaSError",this.code=e,this.domain=s}get isNotFound(){return this.code===404}get isUnauthorized(){return this.code===401}get isForbidden(){return this.code===403}get isConflict(){return this.code===409}get isRateLimited(){return this.code===429}}class y{constructor(e,t){this.onUnauthorized=null,this.baseUrl=e,this.authMode=t}setUnauthorizedHandler(e){this.onUnauthorized=e}async request(e,t,s,r){try{return await this.doRequest(e,t,s,r)}catch(n){if(n instanceof c&&n.isUnauthorized&&this.onUnauthorized&&(r!=null&&r.Authorization)){const i=await this.onUnauthorized();if(i)return this.doRequest(e,t,s,{...r,Authorization:`Bearer ${i}`})}throw n}}async get(e,t){return this.request("GET",e,void 0,t)}async post(e,t,s){return this.request("POST",e,t,s)}async patch(e,t,s){return this.request("PATCH",e,t,s)}async del(e,t){return this.request("DELETE",e,void 0,t)}async uploadBinary(e,t,s){try{return await this.doUploadBinary(e,t,s)}catch(r){if(r instanceof c&&r.isUnauthorized&&this.onUnauthorized&&(s!=null&&s.Authorization)){const n=await this.onUnauthorized();if(n)return this.doUploadBinary(e,t,{...s,Authorization:`Bearer ${n}`})}throw r}}async doUploadBinary(e,t,s){const r={"Content-Type":"application/octet-stream",...this.getAuthHeaders(),...s},i=await(await fetch(`${this.baseUrl}${e}`,{method:"POST",headers:r,body:t})).json();if(i.code&&i.code>=400){const a=this.inferDomain(e);throw new c(i.code,i.message||"Upload failed",a)}return i.data}async doRequest(e,t,s,r){const n={"Content-Type":"application/json",...this.getAuthHeaders(),...r},a=await(await fetch(`${this.baseUrl}${t}`,{method:e,headers:n,body:s?JSON.stringify(s):void 0})).json();if(a.code&&a.code>=400){const p=this.inferDomain(t);throw new c(a.code,a.message||"Request failed",p)}return a.data}getAuthHeaders(){switch(this.authMode.type){case"publishableKey":case"apiKey":return{"X-API-Key":this.authMode.key};case"portalToken":case"embedToken":return{Authorization:`Bearer ${this.authMode.token}`}}}inferDomain(e){return e.startsWith("/auth")?"auth":e.startsWith("/billing")?"billing":e.startsWith("/report")?"report":"unknown"}}class ${constructor(e){this.accessToken=null,this.refreshToken=null,this.refreshTimer=null,this.refreshInFlight=null,this.onRefreshNeeded=null,this.onTokensChanged=null,this.boundHandleStorage=null,this.storageKey=`ss_rt_${e.slice(0,12)}`,this.refreshToken=this.loadRefreshToken(),typeof window<"u"&&(this.boundHandleStorage=this.handleStorageEvent.bind(this),window.addEventListener("storage",this.boundHandleStorage))}setRefreshCallback(e){this.onRefreshNeeded=e}setTokensChangedCallback(e){this.onTokensChanged=e}getAccessToken(){return this.accessToken}getRefreshToken(){return this.refreshToken}hasRefreshToken(){return this.refreshToken!==null}setTokens(e,t){this.accessToken=e,this.refreshToken=t,this.saveRefreshToken(t),this.scheduleRefresh(e)}clearTokens(){this.accessToken=null,this.refreshToken=null,this.removeRefreshToken(),this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=null)}async refreshOnce(){return this.refreshInFlight?this.refreshInFlight:(this.refreshInFlight=this.executeRefresh().finally(()=>{this.refreshInFlight=null}),this.refreshInFlight)}destroy(){this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=null),typeof window<"u"&&this.boundHandleStorage&&(window.removeEventListener("storage",this.boundHandleStorage),this.boundHandleStorage=null)}async executeRefresh(){if(!this.onRefreshNeeded)throw new Error("No refresh callback configured");typeof navigator<"u"&&"locks"in navigator?await navigator.locks.request(`ss_refresh_lock_${this.storageKey}`,async()=>{const e=this.loadRefreshToken();e&&e!==this.refreshToken&&(this.refreshToken=e),await this.onRefreshNeeded()}):await this.onRefreshNeeded()}scheduleRefresh(e){this.refreshTimer&&clearTimeout(this.refreshTimer);const t=this.getTokenExpiry(e);if(!t)return;const s=t*1e3-Date.now()-6e4;if(s<=0){this.refreshOnce().catch(()=>{});return}this.refreshTimer=setTimeout(()=>{this.refreshOnce().catch(()=>{})},s)}handleStorageEvent(e){var t;if(e.key===this.storageKey){if(e.newValue===null){this.accessToken=null,this.refreshToken=null,this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=null),(t=this.onTokensChanged)==null||t.call(this);return}e.newValue!==this.refreshToken&&(this.refreshToken=e.newValue,this.accessToken=null,this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=null))}}getTokenExpiry(e){try{const t=e.split(".")[1];return JSON.parse(atob(t)).exp??null}catch{return null}}loadRefreshToken(){try{return localStorage.getItem(this.storageKey)}catch{return null}}saveRefreshToken(e){try{localStorage.setItem(this.storageKey,e)}catch{}}removeRefreshToken(){try{localStorage.removeItem(this.storageKey)}catch{}}}class R{constructor(){this.listeners=new Map}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{var s;(s=this.listeners.get(e))==null||s.delete(t)}}emit(e,t){var s;(s=this.listeners.get(e))==null||s.forEach(r=>r(t))}removeAll(){this.listeners.clear()}}const T=500,w=600,M=5*60*1e3;class b{constructor(e,t,s,r){this.cachedUser=null,this.cachedSettings=null,this.loaded=!1,this.transport=e,this.tokenManager=t,this.emitter=s,this.baseUrl=r}async load(){var e,t;if(!this.loaded){try{this.cachedSettings=await this.transport.get("/auth/settings")}catch(s){console.warn("[SaaS Support] Failed to load project settings:",s)}if((e=this.tokenManager)!=null&&e.hasRefreshToken())try{await this.performRefresh()}catch{(t=this.tokenManager)==null||t.clearTokens()}this.loaded=!0}}async signIn(e,t){const s=await this.transport.post("/auth/login",{email:e,password:t});if("mfaRequired"in s&&s.mfaRequired)return s;const r=s;return this.setSession(r),r}async signUp(e,t){const s=await this.transport.post("/auth/register",{email:e,password:t});return this.setSession(s),s}async signOut(){var t;const e=(t=this.tokenManager)==null?void 0:t.getRefreshToken();if(e)try{await this.transport.post("/auth/logout",{refreshToken:e})}catch{}this.clearSession()}async signInWithOAuth(e){const t=`${this.baseUrl}/auth/oauth/${e}/popup-callback`,{authUrl:s,state:r}=await this.transport.get(`/auth/oauth/${e}?redirect_uri=${encodeURIComponent(t)}`),n=window.screenX+(window.innerWidth-T)/2,i=window.screenY+(window.innerHeight-w)/2,a=window.open(s,"saas-support-oauth",`width=${T},height=${w},left=${n},top=${i},toolbar=no,menubar=no`);return new Promise((p,l)=>{let h=!1;const d=async u=>{var k;if(((k=u.data)==null?void 0:k.type)==="saas-support:oauth-callback"&&!h){if(h=!0,window.removeEventListener("message",d),clearTimeout(m),clearInterval(f),a==null||a.close(),u.data.error){l(new Error(`OAuth error: ${u.data.error}`));return}try{const g=await this.transport.post(`/auth/oauth/${e}/callback`,{code:u.data.code,state:u.data.state||r});this.setSession(g),p(g)}catch(g){l(g)}}};window.addEventListener("message",d);const m=setTimeout(()=>{h||(h=!0,window.removeEventListener("message",d),clearInterval(f),a==null||a.close(),l(new Error("OAuth popup timed out")))},M),f=setInterval(()=>{a!=null&&a.closed&&!h&&(h=!0,clearInterval(f),clearTimeout(m),window.removeEventListener("message",d),l(new Error("OAuth popup was closed")))},500)})}async submitMfaCode(e,t){const s=await this.transport.post("/auth/login/mfa",{mfaToken:e,code:t});return this.setSession(s),s}async sendMagicLink(e,t){await this.transport.post("/auth/magic-link/send",{email:e,redirectUrl:t})}async verifyMagicLink(e){const t=await this.transport.post("/auth/magic-link/verify",{token:e});return this.setSession(t),t}async sendPasswordReset(e,t){await this.transport.post("/auth/password-reset/send",{email:e,redirectUrl:t})}async resetPassword(e,t){await this.transport.post("/auth/password-reset/verify",{token:e,newPassword:t})}async setupMfa(){return this.transport.post("/auth/mfa/setup",void 0,this.authHeaders())}async verifyMfa(e){return this.transport.post("/auth/mfa/verify",{code:e},this.authHeaders())}async disableMfa(e){await this.transport.post("/auth/mfa/disable",{code:e},this.authHeaders())}async getToken(){var t,s,r;const e=((t=this.tokenManager)==null?void 0:t.getAccessToken())??null;if(e)return e;if((s=this.tokenManager)!=null&&s.hasRefreshToken())try{return await this.tokenManager.refreshOnce(),((r=this.tokenManager)==null?void 0:r.getAccessToken())??null}catch{return this.clearSession(),null}return null}async getUser(){if(this.cachedUser)return this.cachedUser;const e=await this.getToken();if(!e)return null;try{return this.cachedUser=await this.transport.get("/auth/me",{Authorization:`Bearer ${e}`}),this.cachedUser}catch{return null}}getUserSync(){return this.cachedUser}isLoaded(){return this.loaded}async getSettings(){if(this.cachedSettings)return this.cachedSettings;try{return this.cachedSettings=await this.transport.get("/auth/settings"),this.cachedSettings}catch{return null}}onAuthStateChange(e){return this.emitter.on("authStateChange",e)}async updateProfile(e){const t=await this.transport.patch("/auth/me",e,this.authHeaders());return this.cachedUser=t,this.emitter.emit("authStateChange",t),t}async uploadAvatar(e){const t=await this.transport.uploadBinary("/auth/avatar",e,this.authHeaders());return this.cachedUser&&(this.cachedUser={...this.cachedUser,avatarUrl:t.avatarUrl},this.emitter.emit("authStateChange",this.cachedUser)),t}async changePassword(e,t){await this.transport.post("/auth/change-password",{currentPassword:e,newPassword:t},this.authHeaders())}async listOrgs(){return this.transport.get("/auth/orgs",this.authHeaders())}async createOrg(e,t){return this.transport.post("/auth/orgs",{name:e,slug:t},this.authHeaders())}async getOrg(e){return this.transport.get(`/auth/orgs/${e}`,this.authHeaders())}async updateOrg(e,t){return this.transport.patch(`/auth/orgs/${e}`,t,this.authHeaders())}async deleteOrg(e){await this.transport.del(`/auth/orgs/${e}`,this.authHeaders())}async listMembers(e){return this.transport.get(`/auth/orgs/${e}/members`,this.authHeaders())}async sendInvite(e,t,s){return this.transport.post(`/auth/orgs/${e}/invites`,{email:t,role:s},this.authHeaders())}async updateMemberRole(e,t,s){await this.transport.patch(`/auth/orgs/${e}/members/${t}`,{role:s},this.authHeaders())}async removeMember(e,t){await this.transport.del(`/auth/orgs/${e}/members/${t}`,this.authHeaders())}async acceptInvite(e){return this.transport.post(`/auth/invites/${e}/accept`,void 0,this.authHeaders())}handleExternalLogout(){this.cachedUser=null,this.emitter.emit("authStateChange",null)}async performRefresh(){var s;const e=(s=this.tokenManager)==null?void 0:s.getRefreshToken();if(!e)throw new Error("No refresh token");const t=await this.transport.post("/auth/refresh",{refreshToken:e});if(this.tokenManager.setTokens(t.accessToken,t.refreshToken),!this.cachedUser)try{this.cachedUser=await this.transport.get("/auth/me",{Authorization:`Bearer ${t.accessToken}`}),this.emitter.emit("authStateChange",this.cachedUser)}catch{}}setSession(e){var t;(t=this.tokenManager)==null||t.setTokens(e.accessToken,e.refreshToken),this.cachedUser=e.user,this.emitter.emit("authStateChange",e.user)}clearSession(){var e;(e=this.tokenManager)==null||e.clearTokens(),this.cachedUser=null,this.emitter.emit("authStateChange",null)}authHeaders(){var t;const e=(t=this.tokenManager)==null?void 0:t.getAccessToken();return e?{Authorization:`Bearer ${e}`}:{}}}class S{constructor(e){this.transport=e}async createCustomer(e){return this.transport.post("/billing/customers",e)}async getCustomer(e){return this.transport.get(`/billing/customers/${e}`)}async updateCustomer(e,t){return this.transport.patch(`/billing/customers/${e}`,t)}async subscribe(e,t){return this.transport.post(`/billing/customers/${e}/subscribe`,{planId:t})}async changePlan(e,t){return this.transport.patch(`/billing/customers/${e}/subscription`,{planId:t})}async cancelSubscription(e){return this.transport.del(`/billing/customers/${e}/subscription`)}async getInvoices(e){return this.transport.get(`/billing/customers/${e}/invoices`)}async ingestUsageEvent(e){return this.transport.post("/billing/events",e)}async getCurrentUsage(e){return this.transport.get(`/billing/customers/${e}/usage`)}async createPortalToken(e,t){return this.transport.post("/billing/portal-tokens",{customerId:e,expiresIn:t})}async applyCoupon(e,t){return this.transport.post(`/billing/customers/${e}/coupon`,{code:t})}}class U{constructor(e){this.transport=e}async executeQuery(e){return this.transport.post("/report/query",e)}async listQueries(e){const t=e?this.toQueryString(e):"";return this.transport.get(`/report/queries${t}`)}async saveQuery(e){return this.transport.post("/report/queries",e)}async updateQuery(e,t){return this.transport.patch(`/report/queries/${e}`,t)}async deleteQuery(e){await this.transport.del(`/report/queries/${e}`)}async listDashboards(e){const t=e?this.toQueryString(e):"";return this.transport.get(`/report/dashboards${t}`)}async createDashboard(e){return this.transport.post("/report/dashboards",e)}async getDashboard(e){return this.transport.get(`/report/dashboards/${e}`)}async updateDashboard(e,t){return this.transport.patch(`/report/dashboards/${e}`,t)}async deleteDashboard(e){await this.transport.del(`/report/dashboards/${e}`)}async createEmbedToken(e){return this.transport.post("/report/embed-tokens",e)}async listEmbedTokens(){return this.transport.get("/report/embed-tokens")}async revokeEmbedToken(e){await this.transport.del(`/report/embed-tokens/${e}`)}toQueryString(e){const t=Object.entries(e).filter(([,s])=>s!=null&&s!=="");return t.length===0?"":"?"+t.map(([s,r])=>`${s}=${encodeURIComponent(String(r))}`).join("&")}}const v="https://api.saas-support.com/v1";class E{constructor(e){if(this.tokenManager=null,this.loaded=!1,!e.publishableKey&&!e.apiKey)throw new Error("SaaSSupport: either publishableKey or apiKey is required");const t=e.baseUrl??v;this.emitter=new R;const s=e.publishableKey?new y(t,{type:"publishableKey",key:e.publishableKey}):null,r=e.apiKey?new y(t,{type:"apiKey",key:e.apiKey}):null;e.publishableKey&&(this.tokenManager=new $(e.publishableKey)),this.auth=new b(s??r,this.tokenManager,this.emitter,t),this.billing=new S(r??s),this.report=new U(r??s),this.tokenManager&&(this.tokenManager.setRefreshCallback(()=>this.auth.performRefresh()),this.tokenManager.setTokensChangedCallback(()=>{this.tokenManager.hasRefreshToken()||this.auth.handleExternalLogout()})),this.tokenManager&&s&&s.setUnauthorizedHandler(async()=>{try{return await this.tokenManager.refreshOnce(),this.tokenManager.getAccessToken()}catch{return null}})}async load(){this.loaded||(await this.auth.load(),this.loaded=!0)}isLoaded(){return this.loaded}onError(e){return this.emitter.on("error",e)}destroy(){var e;(e=this.tokenManager)==null||e.destroy(),this.emitter.removeAll()}}function H(o){return"mfaRequired"in o&&o.mfaRequired===!0}exports.AuthClient=b;exports.BillingClient=S;exports.ReportClient=U;exports.SaaSError=c;exports.SaaSSupport=E;exports.Transport=y;exports.isMfaRequired=H;
package/dist/index.d.ts CHANGED
@@ -44,6 +44,12 @@ export declare class AuthClient {
44
44
  avatarUrl?: string;
45
45
  metadata?: Record<string, unknown>;
46
46
  }): Promise<User>;
47
+ uploadAvatar(imageBlob: Blob): Promise<{
48
+ avatarUrl: string;
49
+ small: string;
50
+ medium: string;
51
+ original: string;
52
+ }>;
47
53
  changePassword(currentPassword: string, newPassword: string): Promise<void>;
48
54
  listOrgs(): Promise<Org[]>;
49
55
  createOrg(name: string, slug: string): Promise<Org>;
@@ -61,6 +67,7 @@ export declare class AuthClient {
61
67
  orgId: string;
62
68
  role: string;
63
69
  }>;
70
+ /* Excluded from this release type: handleExternalLogout */
64
71
  /* Excluded from this release type: performRefresh */
65
72
  private setSession;
66
73
  private clearSession;
@@ -449,17 +456,28 @@ declare class TokenManager {
449
456
  private accessToken;
450
457
  private refreshToken;
451
458
  private refreshTimer;
459
+ private refreshInFlight;
452
460
  private storageKey;
453
461
  private onRefreshNeeded;
462
+ private onTokensChanged;
463
+ private boundHandleStorage;
454
464
  constructor(keyPrefix: string);
455
465
  setRefreshCallback(cb: () => Promise<void>): void;
466
+ setTokensChangedCallback(cb: () => void): void;
456
467
  getAccessToken(): string | null;
457
468
  getRefreshToken(): string | null;
458
469
  hasRefreshToken(): boolean;
459
470
  setTokens(accessToken: string, refreshToken: string): void;
460
471
  clearTokens(): void;
472
+ /**
473
+ * Coalesces concurrent refresh calls within this tab and coordinates
474
+ * across tabs via Web Locks API (when available).
475
+ */
476
+ refreshOnce(): Promise<void>;
461
477
  destroy(): void;
478
+ private executeRefresh;
462
479
  private scheduleRefresh;
480
+ private handleStorageEvent;
463
481
  private getTokenExpiry;
464
482
  private loadRefreshToken;
465
483
  private saveRefreshToken;
@@ -469,12 +487,18 @@ declare class TokenManager {
469
487
  export declare class Transport {
470
488
  private baseUrl;
471
489
  private authMode;
490
+ private onUnauthorized;
472
491
  constructor(baseUrl: string, authMode: AuthMode);
492
+ /** Register a handler that refreshes tokens and returns a new access token, or null. */
493
+ setUnauthorizedHandler(handler: () => Promise<string | null>): void;
473
494
  request<T>(method: string, path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
474
495
  get<T>(path: string, headers?: Record<string, string>): Promise<T>;
475
496
  post<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
476
497
  patch<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
477
498
  del<T>(path: string, headers?: Record<string, string>): Promise<T>;
499
+ uploadBinary<T>(path: string, data: Blob, headers?: Record<string, string>): Promise<T>;
500
+ private doUploadBinary;
501
+ private doRequest;
478
502
  private getAuthHeaders;
479
503
  private inferDomain;
480
504
  }