@saas-support/react 0.3.1 → 0.3.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.
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 f 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(i){if(i instanceof f&&i.isUnauthorized&&this.onUnauthorized&&(r!=null&&r.Authorization)){const h=await this.onUnauthorized();if(h)return this.doRequest(e,t,s,{...r,Authorization:`Bearer ${h}`})}throw i}}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 doRequest(e,t,s,r){const i={"Content-Type":"application/json",...this.getAuthHeaders(),...r},n=await(await fetch(`${this.baseUrl}${t}`,{method:e,headers:i,body:s?JSON.stringify(s):void 0})).json();if(n.code&&n.code>=400){const g=this.inferDomain(t);throw new f(n.code,n.message||"Request failed",g)}return n.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 U{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)}`),i=window.screenX+(window.innerWidth-T)/2,h=window.screenY+(window.innerHeight-w)/2,n=window.open(s,"saas-support-oauth",`width=${T},height=${w},left=${i},top=${h},toolbar=no,menubar=no`);return new Promise((g,c)=>{let o=!1;const l=async u=>{var m;if(((m=u.data)==null?void 0:m.type)==="saas-support:oauth-callback"&&!o){if(o=!0,window.removeEventListener("message",l),clearTimeout(k),clearInterval(p),n==null||n.close(),u.data.error){c(new Error(`OAuth error: ${u.data.error}`));return}try{const d=await this.transport.post(`/auth/oauth/${e}/callback`,{code:u.data.code,state:u.data.state||r});this.setSession(d),g(d)}catch(d){c(d)}}};window.addEventListener("message",l);const k=setTimeout(()=>{o||(o=!0,window.removeEventListener("message",l),clearInterval(p),n==null||n.close(),c(new Error("OAuth popup timed out")))},M),p=setInterval(()=>{n!=null&&n.closed&&!o&&(o=!0,clearInterval(p),clearTimeout(k),window.removeEventListener("message",l),c(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 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 R{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 E="https://api.saas-support.com/v1";class v{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??E;this.emitter=new U;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 R(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 C(a){return"mfaRequired"in a&&a.mfaRequired===!0}exports.AuthClient=b;exports.BillingClient=S;exports.ReportClient=R;exports.SaaSError=f;exports.SaaSSupport=v;exports.Transport=y;exports.isMfaRequired=C;
package/dist/index.d.ts CHANGED
@@ -61,6 +61,7 @@ export declare class AuthClient {
61
61
  orgId: string;
62
62
  role: string;
63
63
  }>;
64
+ /* Excluded from this release type: handleExternalLogout */
64
65
  /* Excluded from this release type: performRefresh */
65
66
  private setSession;
66
67
  private clearSession;
@@ -449,17 +450,28 @@ declare class TokenManager {
449
450
  private accessToken;
450
451
  private refreshToken;
451
452
  private refreshTimer;
453
+ private refreshInFlight;
452
454
  private storageKey;
453
455
  private onRefreshNeeded;
456
+ private onTokensChanged;
457
+ private boundHandleStorage;
454
458
  constructor(keyPrefix: string);
455
459
  setRefreshCallback(cb: () => Promise<void>): void;
460
+ setTokensChangedCallback(cb: () => void): void;
456
461
  getAccessToken(): string | null;
457
462
  getRefreshToken(): string | null;
458
463
  hasRefreshToken(): boolean;
459
464
  setTokens(accessToken: string, refreshToken: string): void;
460
465
  clearTokens(): void;
466
+ /**
467
+ * Coalesces concurrent refresh calls within this tab and coordinates
468
+ * across tabs via Web Locks API (when available).
469
+ */
470
+ refreshOnce(): Promise<void>;
461
471
  destroy(): void;
472
+ private executeRefresh;
462
473
  private scheduleRefresh;
474
+ private handleStorageEvent;
463
475
  private getTokenExpiry;
464
476
  private loadRefreshToken;
465
477
  private saveRefreshToken;
@@ -469,12 +481,16 @@ declare class TokenManager {
469
481
  export declare class Transport {
470
482
  private baseUrl;
471
483
  private authMode;
484
+ private onUnauthorized;
472
485
  constructor(baseUrl: string, authMode: AuthMode);
486
+ /** Register a handler that refreshes tokens and returns a new access token, or null. */
487
+ setUnauthorizedHandler(handler: () => Promise<string | null>): void;
473
488
  request<T>(method: string, path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
474
489
  get<T>(path: string, headers?: Record<string, string>): Promise<T>;
475
490
  post<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
476
491
  patch<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
477
492
  del<T>(path: string, headers?: Record<string, string>): Promise<T>;
493
+ private doRequest;
478
494
  private getAuthHeaders;
479
495
  private inferDomain;
480
496
  }
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- class b extends Error {
2
- constructor(t, e, s = "unknown") {
3
- super(e), this.name = "SaaSError", this.code = t, this.domain = s;
1
+ class k extends Error {
2
+ constructor(e, t, s = "unknown") {
3
+ super(t), this.name = "SaaSError", this.code = e, this.domain = s;
4
4
  }
5
5
  get isNotFound() {
6
6
  return this.code === 404;
@@ -19,36 +19,55 @@ class b extends Error {
19
19
  }
20
20
  }
21
21
  class m {
22
- constructor(t, e) {
23
- this.baseUrl = t, this.authMode = e;
22
+ constructor(e, t) {
23
+ this.onUnauthorized = null, this.baseUrl = e, this.authMode = t;
24
24
  }
25
- async request(t, e, s, r) {
25
+ /** Register a handler that refreshes tokens and returns a new access token, or null. */
26
+ setUnauthorizedHandler(e) {
27
+ this.onUnauthorized = e;
28
+ }
29
+ async request(e, t, s, r) {
30
+ try {
31
+ return await this.doRequest(e, t, s, r);
32
+ } catch (i) {
33
+ if (i instanceof k && i.isUnauthorized && this.onUnauthorized && (r != null && r.Authorization)) {
34
+ const h = await this.onUnauthorized();
35
+ if (h)
36
+ return this.doRequest(e, t, s, {
37
+ ...r,
38
+ Authorization: `Bearer ${h}`
39
+ });
40
+ }
41
+ throw i;
42
+ }
43
+ }
44
+ async get(e, t) {
45
+ return this.request("GET", e, void 0, t);
46
+ }
47
+ async post(e, t, s) {
48
+ return this.request("POST", e, t, s);
49
+ }
50
+ async patch(e, t, s) {
51
+ return this.request("PATCH", e, t, s);
52
+ }
53
+ async del(e, t) {
54
+ return this.request("DELETE", e, void 0, t);
55
+ }
56
+ async doRequest(e, t, s, r) {
26
57
  const i = {
27
58
  "Content-Type": "application/json",
28
59
  ...this.getAuthHeaders(),
29
60
  ...r
30
- }, a = await (await fetch(`${this.baseUrl}${e}`, {
31
- method: t,
61
+ }, n = await (await fetch(`${this.baseUrl}${t}`, {
62
+ method: e,
32
63
  headers: i,
33
64
  body: s ? JSON.stringify(s) : void 0
34
65
  })).json();
35
- if (a.code && a.code >= 400) {
36
- const d = this.inferDomain(e);
37
- throw new b(a.code, a.message || "Request failed", d);
66
+ if (n.code && n.code >= 400) {
67
+ const g = this.inferDomain(t);
68
+ throw new k(n.code, n.message || "Request failed", g);
38
69
  }
39
- return a.data;
40
- }
41
- async get(t, e) {
42
- return this.request("GET", t, void 0, e);
43
- }
44
- async post(t, e, s) {
45
- return this.request("POST", t, e, s);
46
- }
47
- async patch(t, e, s) {
48
- return this.request("PATCH", t, e, s);
49
- }
50
- async del(t, e) {
51
- return this.request("DELETE", t, void 0, e);
70
+ return n.data;
52
71
  }
53
72
  getAuthHeaders() {
54
73
  switch (this.authMode.type) {
@@ -60,16 +79,19 @@ class m {
60
79
  return { Authorization: `Bearer ${this.authMode.token}` };
61
80
  }
62
81
  }
63
- inferDomain(t) {
64
- return t.startsWith("/auth") ? "auth" : t.startsWith("/billing") ? "billing" : t.startsWith("/report") ? "report" : "unknown";
82
+ inferDomain(e) {
83
+ return e.startsWith("/auth") ? "auth" : e.startsWith("/billing") ? "billing" : e.startsWith("/report") ? "report" : "unknown";
65
84
  }
66
85
  }
67
- class w {
68
- constructor(t) {
69
- this.accessToken = null, this.refreshToken = null, this.refreshTimer = null, this.onRefreshNeeded = null, this.storageKey = `ss_rt_${t.slice(0, 12)}`, this.refreshToken = this.loadRefreshToken();
86
+ class b {
87
+ constructor(e) {
88
+ 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));
89
+ }
90
+ setRefreshCallback(e) {
91
+ this.onRefreshNeeded = e;
70
92
  }
71
- setRefreshCallback(t) {
72
- this.onRefreshNeeded = t;
93
+ setTokensChangedCallback(e) {
94
+ this.onTokensChanged = e;
73
95
  }
74
96
  getAccessToken() {
75
97
  return this.accessToken;
@@ -80,34 +102,64 @@ class w {
80
102
  hasRefreshToken() {
81
103
  return this.refreshToken !== null;
82
104
  }
83
- setTokens(t, e) {
84
- this.accessToken = t, this.refreshToken = e, this.saveRefreshToken(e), this.scheduleRefresh(t);
105
+ setTokens(e, t) {
106
+ this.accessToken = e, this.refreshToken = t, this.saveRefreshToken(t), this.scheduleRefresh(e);
85
107
  }
86
108
  clearTokens() {
87
109
  this.accessToken = null, this.refreshToken = null, this.removeRefreshToken(), this.refreshTimer && (clearTimeout(this.refreshTimer), this.refreshTimer = null);
88
110
  }
111
+ /**
112
+ * Coalesces concurrent refresh calls within this tab and coordinates
113
+ * across tabs via Web Locks API (when available).
114
+ */
115
+ async refreshOnce() {
116
+ return this.refreshInFlight ? this.refreshInFlight : (this.refreshInFlight = this.executeRefresh().finally(() => {
117
+ this.refreshInFlight = null;
118
+ }), this.refreshInFlight);
119
+ }
89
120
  destroy() {
90
- this.refreshTimer && (clearTimeout(this.refreshTimer), this.refreshTimer = null);
121
+ this.refreshTimer && (clearTimeout(this.refreshTimer), this.refreshTimer = null), typeof window < "u" && this.boundHandleStorage && (window.removeEventListener("storage", this.boundHandleStorage), this.boundHandleStorage = null);
122
+ }
123
+ async executeRefresh() {
124
+ if (!this.onRefreshNeeded)
125
+ throw new Error("No refresh callback configured");
126
+ typeof navigator < "u" && "locks" in navigator ? await navigator.locks.request(
127
+ `ss_refresh_lock_${this.storageKey}`,
128
+ async () => {
129
+ const e = this.loadRefreshToken();
130
+ e && e !== this.refreshToken && (this.refreshToken = e), await this.onRefreshNeeded();
131
+ }
132
+ ) : await this.onRefreshNeeded();
91
133
  }
92
- scheduleRefresh(t) {
93
- var r;
134
+ scheduleRefresh(e) {
94
135
  this.refreshTimer && clearTimeout(this.refreshTimer);
95
- const e = this.getTokenExpiry(t);
96
- if (!e) return;
97
- const s = e * 1e3 - Date.now() - 6e4;
136
+ const t = this.getTokenExpiry(e);
137
+ if (!t) return;
138
+ const s = t * 1e3 - Date.now() - 6e4;
98
139
  if (s <= 0) {
99
- (r = this.onRefreshNeeded) == null || r.call(this);
140
+ this.refreshOnce().catch(() => {
141
+ });
100
142
  return;
101
143
  }
102
144
  this.refreshTimer = setTimeout(() => {
103
- var i;
104
- (i = this.onRefreshNeeded) == null || i.call(this);
145
+ this.refreshOnce().catch(() => {
146
+ });
105
147
  }, s);
106
148
  }
107
- getTokenExpiry(t) {
149
+ handleStorageEvent(e) {
150
+ var t;
151
+ if (e.key === this.storageKey) {
152
+ if (e.newValue === null) {
153
+ this.accessToken = null, this.refreshToken = null, this.refreshTimer && (clearTimeout(this.refreshTimer), this.refreshTimer = null), (t = this.onTokensChanged) == null || t.call(this);
154
+ return;
155
+ }
156
+ e.newValue !== this.refreshToken && (this.refreshToken = e.newValue, this.accessToken = null, this.refreshTimer && (clearTimeout(this.refreshTimer), this.refreshTimer = null));
157
+ }
158
+ }
159
+ getTokenExpiry(e) {
108
160
  try {
109
- const e = t.split(".")[1];
110
- return JSON.parse(atob(e)).exp ?? null;
161
+ const t = e.split(".")[1];
162
+ return JSON.parse(atob(t)).exp ?? null;
111
163
  } catch {
112
164
  return null;
113
165
  }
@@ -119,9 +171,9 @@ class w {
119
171
  return null;
120
172
  }
121
173
  }
122
- saveRefreshToken(t) {
174
+ saveRefreshToken(e) {
123
175
  try {
124
- localStorage.setItem(this.storageKey, t);
176
+ localStorage.setItem(this.storageKey, e);
125
177
  } catch {
126
178
  }
127
179
  }
@@ -136,41 +188,41 @@ class S {
136
188
  constructor() {
137
189
  this.listeners = /* @__PURE__ */ new Map();
138
190
  }
139
- on(t, e) {
140
- return this.listeners.has(t) || this.listeners.set(t, /* @__PURE__ */ new Set()), this.listeners.get(t).add(e), () => {
191
+ on(e, t) {
192
+ return this.listeners.has(e) || this.listeners.set(e, /* @__PURE__ */ new Set()), this.listeners.get(e).add(t), () => {
141
193
  var s;
142
- (s = this.listeners.get(t)) == null || s.delete(e);
194
+ (s = this.listeners.get(e)) == null || s.delete(t);
143
195
  };
144
196
  }
145
- emit(t, e) {
197
+ emit(e, t) {
146
198
  var s;
147
- (s = this.listeners.get(t)) == null || s.forEach((r) => r(e));
199
+ (s = this.listeners.get(e)) == null || s.forEach((r) => r(t));
148
200
  }
149
201
  removeAll() {
150
202
  this.listeners.clear();
151
203
  }
152
204
  }
153
- const k = 500, T = 600, $ = 5 * 60 * 1e3;
205
+ const T = 500, w = 600, $ = 5 * 60 * 1e3;
154
206
  class R {
155
- constructor(t, e, s, r) {
156
- this.cachedUser = null, this.cachedSettings = null, this.loaded = !1, this.transport = t, this.tokenManager = e, this.emitter = s, this.baseUrl = r;
207
+ constructor(e, t, s, r) {
208
+ this.cachedUser = null, this.cachedSettings = null, this.loaded = !1, this.transport = e, this.tokenManager = t, this.emitter = s, this.baseUrl = r;
157
209
  }
158
210
  // ---------------------------------------------------------------------------
159
211
  // Lifecycle
160
212
  // ---------------------------------------------------------------------------
161
213
  async load() {
162
- var t, e;
214
+ var e, t;
163
215
  if (!this.loaded) {
164
216
  try {
165
217
  this.cachedSettings = await this.transport.get("/auth/settings");
166
218
  } catch (s) {
167
219
  console.warn("[SaaS Support] Failed to load project settings:", s);
168
220
  }
169
- if ((t = this.tokenManager) != null && t.hasRefreshToken())
221
+ if ((e = this.tokenManager) != null && e.hasRefreshToken())
170
222
  try {
171
223
  await this.performRefresh();
172
224
  } catch {
173
- (e = this.tokenManager) == null || e.clearTokens();
225
+ (t = this.tokenManager) == null || t.clearTokens();
174
226
  }
175
227
  this.loaded = !0;
176
228
  }
@@ -178,82 +230,82 @@ class R {
178
230
  // ---------------------------------------------------------------------------
179
231
  // Core auth operations
180
232
  // ---------------------------------------------------------------------------
181
- async signIn(t, e) {
182
- const s = await this.transport.post("/auth/login", { email: t, password: e });
233
+ async signIn(e, t) {
234
+ const s = await this.transport.post("/auth/login", { email: e, password: t });
183
235
  if ("mfaRequired" in s && s.mfaRequired)
184
236
  return s;
185
237
  const r = s;
186
238
  return this.setSession(r), r;
187
239
  }
188
- async signUp(t, e) {
189
- const s = await this.transport.post("/auth/register", { email: t, password: e });
240
+ async signUp(e, t) {
241
+ const s = await this.transport.post("/auth/register", { email: e, password: t });
190
242
  return this.setSession(s), s;
191
243
  }
192
244
  async signOut() {
193
- var e;
194
- const t = (e = this.tokenManager) == null ? void 0 : e.getRefreshToken();
195
- if (t)
245
+ var t;
246
+ const e = (t = this.tokenManager) == null ? void 0 : t.getRefreshToken();
247
+ if (e)
196
248
  try {
197
- await this.transport.post("/auth/logout", { refreshToken: t });
249
+ await this.transport.post("/auth/logout", { refreshToken: e });
198
250
  } catch {
199
251
  }
200
252
  this.clearSession();
201
253
  }
202
- async signInWithOAuth(t) {
203
- const e = `${this.baseUrl}/auth/oauth/${t}/popup-callback`, { authUrl: s, state: r } = await this.transport.get(
204
- `/auth/oauth/${t}?redirect_uri=${encodeURIComponent(e)}`
205
- ), i = window.screenX + (window.innerWidth - k) / 2, g = window.screenY + (window.innerHeight - T) / 2, a = window.open(
254
+ async signInWithOAuth(e) {
255
+ const t = `${this.baseUrl}/auth/oauth/${e}/popup-callback`, { authUrl: s, state: r } = await this.transport.get(
256
+ `/auth/oauth/${e}?redirect_uri=${encodeURIComponent(t)}`
257
+ ), i = window.screenX + (window.innerWidth - T) / 2, h = window.screenY + (window.innerHeight - w) / 2, n = window.open(
206
258
  s,
207
259
  "saas-support-oauth",
208
- `width=${k},height=${T},left=${i},top=${g},toolbar=no,menubar=no`
260
+ `width=${T},height=${w},left=${i},top=${h},toolbar=no,menubar=no`
209
261
  );
210
- return new Promise((d, c) => {
262
+ return new Promise((g, c) => {
211
263
  let o = !1;
212
- const u = async (h) => {
213
- var f;
214
- if (((f = h.data) == null ? void 0 : f.type) === "saas-support:oauth-callback" && !o) {
215
- if (o = !0, window.removeEventListener("message", u), clearTimeout(y), clearInterval(p), a == null || a.close(), h.data.error) {
216
- c(new Error(`OAuth error: ${h.data.error}`));
264
+ const l = async (u) => {
265
+ var y;
266
+ if (((y = u.data) == null ? void 0 : y.type) === "saas-support:oauth-callback" && !o) {
267
+ if (o = !0, window.removeEventListener("message", l), clearTimeout(f), clearInterval(p), n == null || n.close(), u.data.error) {
268
+ c(new Error(`OAuth error: ${u.data.error}`));
217
269
  return;
218
270
  }
219
271
  try {
220
- const l = await this.transport.post(
221
- `/auth/oauth/${t}/callback`,
222
- { code: h.data.code, state: h.data.state || r }
272
+ const d = await this.transport.post(
273
+ `/auth/oauth/${e}/callback`,
274
+ { code: u.data.code, state: u.data.state || r }
223
275
  );
224
- this.setSession(l), d(l);
225
- } catch (l) {
226
- c(l);
276
+ this.setSession(d), g(d);
277
+ } catch (d) {
278
+ c(d);
227
279
  }
228
280
  }
229
281
  };
230
- window.addEventListener("message", u);
231
- const y = setTimeout(() => {
232
- o || (o = !0, window.removeEventListener("message", u), clearInterval(p), a == null || a.close(), c(new Error("OAuth popup timed out")));
282
+ window.addEventListener("message", l);
283
+ const f = setTimeout(() => {
284
+ o || (o = !0, window.removeEventListener("message", l), clearInterval(p), n == null || n.close(), c(new Error("OAuth popup timed out")));
233
285
  }, $), p = setInterval(() => {
234
- a != null && a.closed && !o && (o = !0, clearInterval(p), clearTimeout(y), window.removeEventListener("message", u), c(new Error("OAuth popup was closed")));
286
+ n != null && n.closed && !o && (o = !0, clearInterval(p), clearTimeout(f), window.removeEventListener("message", l), c(new Error("OAuth popup was closed")));
235
287
  }, 500);
236
288
  });
237
289
  }
238
- async submitMfaCode(t, e) {
239
- const s = await this.transport.post("/auth/login/mfa", { mfaToken: t, code: e });
290
+ async submitMfaCode(e, t) {
291
+ const s = await this.transport.post("/auth/login/mfa", { mfaToken: e, code: t });
240
292
  return this.setSession(s), s;
241
293
  }
242
294
  // ---------------------------------------------------------------------------
243
295
  // Magic link & password reset
244
296
  // ---------------------------------------------------------------------------
245
- async sendMagicLink(t, e) {
246
- await this.transport.post("/auth/magic-link/send", { email: t, redirectUrl: e });
297
+ async sendMagicLink(e, t) {
298
+ await this.transport.post("/auth/magic-link/send", { email: e, redirectUrl: t });
247
299
  }
248
- async verifyMagicLink(t) {
249
- const e = await this.transport.post("/auth/magic-link/verify", { token: t });
250
- return this.setSession(e), e;
300
+ async verifyMagicLink(e) {
301
+ const t = await this.transport.post("/auth/magic-link/verify", { token: e });
302
+ return this.setSession(t), t;
251
303
  }
252
- async sendPasswordReset(t, e) {
253
- await this.transport.post("/auth/password-reset/send", { email: t, redirectUrl: e });
304
+ async sendPasswordReset(e, t) {
305
+ await this.transport.post("/auth/password-reset/send", { email: e, redirectUrl: t });
254
306
  }
255
- async resetPassword(t, e) {
256
- await this.transport.post("/auth/password-reset/verify", { token: t, newPassword: e });
307
+ async resetPassword(e, t) {
308
+ await this.transport.post("/auth/password-reset/verify", { token: e, newPassword: t });
257
309
  }
258
310
  // ---------------------------------------------------------------------------
259
311
  // MFA management
@@ -261,22 +313,22 @@ class R {
261
313
  async setupMfa() {
262
314
  return this.transport.post("/auth/mfa/setup", void 0, this.authHeaders());
263
315
  }
264
- async verifyMfa(t) {
265
- return this.transport.post("/auth/mfa/verify", { code: t }, this.authHeaders());
316
+ async verifyMfa(e) {
317
+ return this.transport.post("/auth/mfa/verify", { code: e }, this.authHeaders());
266
318
  }
267
- async disableMfa(t) {
268
- await this.transport.post("/auth/mfa/disable", { code: t }, this.authHeaders());
319
+ async disableMfa(e) {
320
+ await this.transport.post("/auth/mfa/disable", { code: e }, this.authHeaders());
269
321
  }
270
322
  // ---------------------------------------------------------------------------
271
323
  // Token & user access
272
324
  // ---------------------------------------------------------------------------
273
325
  async getToken() {
274
- var e, s, r;
275
- const t = ((e = this.tokenManager) == null ? void 0 : e.getAccessToken()) ?? null;
276
- if (t) return t;
326
+ var t, s, r;
327
+ const e = ((t = this.tokenManager) == null ? void 0 : t.getAccessToken()) ?? null;
328
+ if (e) return e;
277
329
  if ((s = this.tokenManager) != null && s.hasRefreshToken())
278
330
  try {
279
- return await this.performRefresh(), ((r = this.tokenManager) == null ? void 0 : r.getAccessToken()) ?? null;
331
+ return await this.tokenManager.refreshOnce(), ((r = this.tokenManager) == null ? void 0 : r.getAccessToken()) ?? null;
280
332
  } catch {
281
333
  return this.clearSession(), null;
282
334
  }
@@ -284,10 +336,10 @@ class R {
284
336
  }
285
337
  async getUser() {
286
338
  if (this.cachedUser) return this.cachedUser;
287
- const t = await this.getToken();
288
- if (!t) return null;
339
+ const e = await this.getToken();
340
+ if (!e) return null;
289
341
  try {
290
- return this.cachedUser = await this.transport.get("/auth/me", { Authorization: `Bearer ${t}` }), this.cachedUser;
342
+ return this.cachedUser = await this.transport.get("/auth/me", { Authorization: `Bearer ${e}` }), this.cachedUser;
291
343
  } catch {
292
344
  return null;
293
345
  }
@@ -306,18 +358,18 @@ class R {
306
358
  return null;
307
359
  }
308
360
  }
309
- onAuthStateChange(t) {
310
- return this.emitter.on("authStateChange", t);
361
+ onAuthStateChange(e) {
362
+ return this.emitter.on("authStateChange", e);
311
363
  }
312
364
  // ---------------------------------------------------------------------------
313
365
  // Profile
314
366
  // ---------------------------------------------------------------------------
315
- async updateProfile(t) {
316
- const e = await this.transport.patch("/auth/me", t, this.authHeaders());
317
- return this.cachedUser = e, this.emitter.emit("authStateChange", e), e;
367
+ async updateProfile(e) {
368
+ const t = await this.transport.patch("/auth/me", e, this.authHeaders());
369
+ return this.cachedUser = t, this.emitter.emit("authStateChange", t), t;
318
370
  }
319
- async changePassword(t, e) {
320
- await this.transport.post("/auth/change-password", { currentPassword: t, newPassword: e }, this.authHeaders());
371
+ async changePassword(e, t) {
372
+ await this.transport.post("/auth/change-password", { currentPassword: e, newPassword: t }, this.authHeaders());
321
373
  }
322
374
  // ---------------------------------------------------------------------------
323
375
  // Organizations
@@ -325,180 +377,192 @@ class R {
325
377
  async listOrgs() {
326
378
  return this.transport.get("/auth/orgs", this.authHeaders());
327
379
  }
328
- async createOrg(t, e) {
329
- return this.transport.post("/auth/orgs", { name: t, slug: e }, this.authHeaders());
380
+ async createOrg(e, t) {
381
+ return this.transport.post("/auth/orgs", { name: e, slug: t }, this.authHeaders());
330
382
  }
331
- async getOrg(t) {
332
- return this.transport.get(`/auth/orgs/${t}`, this.authHeaders());
383
+ async getOrg(e) {
384
+ return this.transport.get(`/auth/orgs/${e}`, this.authHeaders());
333
385
  }
334
- async updateOrg(t, e) {
335
- return this.transport.patch(`/auth/orgs/${t}`, e, this.authHeaders());
386
+ async updateOrg(e, t) {
387
+ return this.transport.patch(`/auth/orgs/${e}`, t, this.authHeaders());
336
388
  }
337
- async deleteOrg(t) {
338
- await this.transport.del(`/auth/orgs/${t}`, this.authHeaders());
389
+ async deleteOrg(e) {
390
+ await this.transport.del(`/auth/orgs/${e}`, this.authHeaders());
339
391
  }
340
392
  // ---------------------------------------------------------------------------
341
393
  // Members & Invites
342
394
  // ---------------------------------------------------------------------------
343
- async listMembers(t) {
344
- return this.transport.get(`/auth/orgs/${t}/members`, this.authHeaders());
395
+ async listMembers(e) {
396
+ return this.transport.get(`/auth/orgs/${e}/members`, this.authHeaders());
345
397
  }
346
- async sendInvite(t, e, s) {
347
- return this.transport.post(`/auth/orgs/${t}/invites`, { email: e, role: s }, this.authHeaders());
398
+ async sendInvite(e, t, s) {
399
+ return this.transport.post(`/auth/orgs/${e}/invites`, { email: t, role: s }, this.authHeaders());
348
400
  }
349
- async updateMemberRole(t, e, s) {
350
- await this.transport.patch(`/auth/orgs/${t}/members/${e}`, { role: s }, this.authHeaders());
401
+ async updateMemberRole(e, t, s) {
402
+ await this.transport.patch(`/auth/orgs/${e}/members/${t}`, { role: s }, this.authHeaders());
351
403
  }
352
- async removeMember(t, e) {
353
- await this.transport.del(`/auth/orgs/${t}/members/${e}`, this.authHeaders());
404
+ async removeMember(e, t) {
405
+ await this.transport.del(`/auth/orgs/${e}/members/${t}`, this.authHeaders());
354
406
  }
355
- async acceptInvite(t) {
356
- return this.transport.post(`/auth/invites/${t}/accept`, void 0, this.authHeaders());
407
+ async acceptInvite(e) {
408
+ return this.transport.post(`/auth/invites/${e}/accept`, void 0, this.authHeaders());
357
409
  }
358
410
  // ---------------------------------------------------------------------------
359
411
  // Internal
360
412
  // ---------------------------------------------------------------------------
413
+ /** @internal Called when another tab logs out (via storage event). */
414
+ handleExternalLogout() {
415
+ this.cachedUser = null, this.emitter.emit("authStateChange", null);
416
+ }
361
417
  /** @internal */
362
418
  async performRefresh() {
363
419
  var s;
364
- const t = (s = this.tokenManager) == null ? void 0 : s.getRefreshToken();
365
- if (!t) throw new Error("No refresh token");
366
- const e = await this.transport.post(
420
+ const e = (s = this.tokenManager) == null ? void 0 : s.getRefreshToken();
421
+ if (!e) throw new Error("No refresh token");
422
+ const t = await this.transport.post(
367
423
  "/auth/refresh",
368
- { refreshToken: t }
424
+ { refreshToken: e }
369
425
  );
370
- if (this.tokenManager.setTokens(e.accessToken, e.refreshToken), !this.cachedUser)
426
+ if (this.tokenManager.setTokens(t.accessToken, t.refreshToken), !this.cachedUser)
371
427
  try {
372
- this.cachedUser = await this.transport.get("/auth/me", { Authorization: `Bearer ${e.accessToken}` }), this.emitter.emit("authStateChange", this.cachedUser);
428
+ this.cachedUser = await this.transport.get("/auth/me", { Authorization: `Bearer ${t.accessToken}` }), this.emitter.emit("authStateChange", this.cachedUser);
373
429
  } catch {
374
430
  }
375
431
  }
376
- setSession(t) {
377
- var e;
378
- (e = this.tokenManager) == null || e.setTokens(t.accessToken, t.refreshToken), this.cachedUser = t.user, this.emitter.emit("authStateChange", t.user);
432
+ setSession(e) {
433
+ var t;
434
+ (t = this.tokenManager) == null || t.setTokens(e.accessToken, e.refreshToken), this.cachedUser = e.user, this.emitter.emit("authStateChange", e.user);
379
435
  }
380
436
  clearSession() {
381
- var t;
382
- (t = this.tokenManager) == null || t.clearTokens(), this.cachedUser = null, this.emitter.emit("authStateChange", null);
437
+ var e;
438
+ (e = this.tokenManager) == null || e.clearTokens(), this.cachedUser = null, this.emitter.emit("authStateChange", null);
383
439
  }
384
440
  authHeaders() {
385
- var e;
386
- const t = (e = this.tokenManager) == null ? void 0 : e.getAccessToken();
387
- return t ? { Authorization: `Bearer ${t}` } : {};
441
+ var t;
442
+ const e = (t = this.tokenManager) == null ? void 0 : t.getAccessToken();
443
+ return e ? { Authorization: `Bearer ${e}` } : {};
388
444
  }
389
445
  }
390
446
  class U {
391
- constructor(t) {
392
- this.transport = t;
447
+ constructor(e) {
448
+ this.transport = e;
393
449
  }
394
450
  // --- Customer ---
395
- async createCustomer(t) {
396
- return this.transport.post("/billing/customers", t);
451
+ async createCustomer(e) {
452
+ return this.transport.post("/billing/customers", e);
397
453
  }
398
- async getCustomer(t) {
399
- return this.transport.get(`/billing/customers/${t}`);
454
+ async getCustomer(e) {
455
+ return this.transport.get(`/billing/customers/${e}`);
400
456
  }
401
- async updateCustomer(t, e) {
402
- return this.transport.patch(`/billing/customers/${t}`, e);
457
+ async updateCustomer(e, t) {
458
+ return this.transport.patch(`/billing/customers/${e}`, t);
403
459
  }
404
460
  // --- Subscription ---
405
- async subscribe(t, e) {
406
- return this.transport.post(`/billing/customers/${t}/subscribe`, { planId: e });
461
+ async subscribe(e, t) {
462
+ return this.transport.post(`/billing/customers/${e}/subscribe`, { planId: t });
407
463
  }
408
- async changePlan(t, e) {
409
- return this.transport.patch(`/billing/customers/${t}/subscription`, { planId: e });
464
+ async changePlan(e, t) {
465
+ return this.transport.patch(`/billing/customers/${e}/subscription`, { planId: t });
410
466
  }
411
- async cancelSubscription(t) {
412
- return this.transport.del(`/billing/customers/${t}/subscription`);
467
+ async cancelSubscription(e) {
468
+ return this.transport.del(`/billing/customers/${e}/subscription`);
413
469
  }
414
470
  // --- Invoices ---
415
- async getInvoices(t) {
416
- return this.transport.get(`/billing/customers/${t}/invoices`);
471
+ async getInvoices(e) {
472
+ return this.transport.get(`/billing/customers/${e}/invoices`);
417
473
  }
418
474
  // --- Usage ---
419
- async ingestUsageEvent(t) {
420
- return this.transport.post("/billing/events", t);
475
+ async ingestUsageEvent(e) {
476
+ return this.transport.post("/billing/events", e);
421
477
  }
422
- async getCurrentUsage(t) {
423
- return this.transport.get(`/billing/customers/${t}/usage`);
478
+ async getCurrentUsage(e) {
479
+ return this.transport.get(`/billing/customers/${e}/usage`);
424
480
  }
425
481
  // --- Portal ---
426
- async createPortalToken(t, e) {
427
- return this.transport.post("/billing/portal-tokens", { customerId: t, expiresIn: e });
482
+ async createPortalToken(e, t) {
483
+ return this.transport.post("/billing/portal-tokens", { customerId: e, expiresIn: t });
428
484
  }
429
485
  // --- Coupon ---
430
- async applyCoupon(t, e) {
431
- return this.transport.post(`/billing/customers/${t}/coupon`, { code: e });
486
+ async applyCoupon(e, t) {
487
+ return this.transport.post(`/billing/customers/${e}/coupon`, { code: t });
432
488
  }
433
489
  }
434
490
  class M {
435
- constructor(t) {
436
- this.transport = t;
491
+ constructor(e) {
492
+ this.transport = e;
437
493
  }
438
494
  // --- Query ---
439
- async executeQuery(t) {
440
- return this.transport.post("/report/query", t);
495
+ async executeQuery(e) {
496
+ return this.transport.post("/report/query", e);
441
497
  }
442
498
  // --- Saved Queries ---
443
- async listQueries(t) {
444
- const e = t ? this.toQueryString(t) : "";
445
- return this.transport.get(`/report/queries${e}`);
499
+ async listQueries(e) {
500
+ const t = e ? this.toQueryString(e) : "";
501
+ return this.transport.get(`/report/queries${t}`);
446
502
  }
447
- async saveQuery(t) {
448
- return this.transport.post("/report/queries", t);
503
+ async saveQuery(e) {
504
+ return this.transport.post("/report/queries", e);
449
505
  }
450
- async updateQuery(t, e) {
451
- return this.transport.patch(`/report/queries/${t}`, e);
506
+ async updateQuery(e, t) {
507
+ return this.transport.patch(`/report/queries/${e}`, t);
452
508
  }
453
- async deleteQuery(t) {
454
- await this.transport.del(`/report/queries/${t}`);
509
+ async deleteQuery(e) {
510
+ await this.transport.del(`/report/queries/${e}`);
455
511
  }
456
512
  // --- Dashboards ---
457
- async listDashboards(t) {
458
- const e = t ? this.toQueryString(t) : "";
459
- return this.transport.get(`/report/dashboards${e}`);
513
+ async listDashboards(e) {
514
+ const t = e ? this.toQueryString(e) : "";
515
+ return this.transport.get(`/report/dashboards${t}`);
460
516
  }
461
- async createDashboard(t) {
462
- return this.transport.post("/report/dashboards", t);
517
+ async createDashboard(e) {
518
+ return this.transport.post("/report/dashboards", e);
463
519
  }
464
- async getDashboard(t) {
465
- return this.transport.get(`/report/dashboards/${t}`);
520
+ async getDashboard(e) {
521
+ return this.transport.get(`/report/dashboards/${e}`);
466
522
  }
467
- async updateDashboard(t, e) {
468
- return this.transport.patch(`/report/dashboards/${t}`, e);
523
+ async updateDashboard(e, t) {
524
+ return this.transport.patch(`/report/dashboards/${e}`, t);
469
525
  }
470
- async deleteDashboard(t) {
471
- await this.transport.del(`/report/dashboards/${t}`);
526
+ async deleteDashboard(e) {
527
+ await this.transport.del(`/report/dashboards/${e}`);
472
528
  }
473
529
  // --- Embed Tokens ---
474
- async createEmbedToken(t) {
475
- return this.transport.post("/report/embed-tokens", t);
530
+ async createEmbedToken(e) {
531
+ return this.transport.post("/report/embed-tokens", e);
476
532
  }
477
533
  async listEmbedTokens() {
478
534
  return this.transport.get("/report/embed-tokens");
479
535
  }
480
- async revokeEmbedToken(t) {
481
- await this.transport.del(`/report/embed-tokens/${t}`);
536
+ async revokeEmbedToken(e) {
537
+ await this.transport.del(`/report/embed-tokens/${e}`);
482
538
  }
483
- toQueryString(t) {
484
- const e = Object.entries(t).filter(([, s]) => s != null && s !== "");
485
- return e.length === 0 ? "" : "?" + e.map(([s, r]) => `${s}=${encodeURIComponent(String(r))}`).join("&");
539
+ toQueryString(e) {
540
+ const t = Object.entries(e).filter(([, s]) => s != null && s !== "");
541
+ return t.length === 0 ? "" : "?" + t.map(([s, r]) => `${s}=${encodeURIComponent(String(r))}`).join("&");
486
542
  }
487
543
  }
488
- const v = "https://api.saas-support.com/v1";
489
- class E {
490
- constructor(t) {
491
- if (this.tokenManager = null, this.loaded = !1, !t.publishableKey && !t.apiKey)
544
+ const E = "https://api.saas-support.com/v1";
545
+ class v {
546
+ constructor(e) {
547
+ if (this.tokenManager = null, this.loaded = !1, !e.publishableKey && !e.apiKey)
492
548
  throw new Error("SaaSSupport: either publishableKey or apiKey is required");
493
- const e = t.baseUrl ?? v;
549
+ const t = e.baseUrl ?? E;
494
550
  this.emitter = new S();
495
- const s = t.publishableKey ? new m(e, { type: "publishableKey", key: t.publishableKey }) : null, r = t.apiKey ? new m(e, { type: "apiKey", key: t.apiKey }) : null;
496
- t.publishableKey && (this.tokenManager = new w(t.publishableKey)), this.auth = new R(
551
+ const s = e.publishableKey ? new m(t, { type: "publishableKey", key: e.publishableKey }) : null, r = e.apiKey ? new m(t, { type: "apiKey", key: e.apiKey }) : null;
552
+ e.publishableKey && (this.tokenManager = new b(e.publishableKey)), this.auth = new R(
497
553
  s ?? r,
498
554
  this.tokenManager,
499
555
  this.emitter,
500
- e
501
- ), this.billing = new U(r ?? s), this.report = new M(r ?? s), this.tokenManager && this.tokenManager.setRefreshCallback(() => this.auth.performRefresh());
556
+ t
557
+ ), this.billing = new U(r ?? s), this.report = new M(r ?? s), this.tokenManager && (this.tokenManager.setRefreshCallback(() => this.auth.performRefresh()), this.tokenManager.setTokensChangedCallback(() => {
558
+ this.tokenManager.hasRefreshToken() || this.auth.handleExternalLogout();
559
+ })), this.tokenManager && s && s.setUnauthorizedHandler(async () => {
560
+ try {
561
+ return await this.tokenManager.refreshOnce(), this.tokenManager.getAccessToken();
562
+ } catch {
563
+ return null;
564
+ }
565
+ });
502
566
  }
503
567
  async load() {
504
568
  this.loaded || (await this.auth.load(), this.loaded = !0);
@@ -506,23 +570,23 @@ class E {
506
570
  isLoaded() {
507
571
  return this.loaded;
508
572
  }
509
- onError(t) {
510
- return this.emitter.on("error", t);
573
+ onError(e) {
574
+ return this.emitter.on("error", e);
511
575
  }
512
576
  destroy() {
513
- var t;
514
- (t = this.tokenManager) == null || t.destroy(), this.emitter.removeAll();
577
+ var e;
578
+ (e = this.tokenManager) == null || e.destroy(), this.emitter.removeAll();
515
579
  }
516
580
  }
517
- function H(n) {
518
- return "mfaRequired" in n && n.mfaRequired === !0;
581
+ function H(a) {
582
+ return "mfaRequired" in a && a.mfaRequired === !0;
519
583
  }
520
584
  export {
521
585
  R as AuthClient,
522
586
  U as BillingClient,
523
587
  M as ReportClient,
524
- b as SaaSError,
525
- E as SaaSSupport,
588
+ k as SaaSError,
589
+ v as SaaSSupport,
526
590
  m as Transport,
527
591
  H as isMfaRequired
528
592
  };
package/dist/react.d.ts CHANGED
@@ -67,6 +67,7 @@ declare class AuthClient {
67
67
  orgId: string;
68
68
  role: string;
69
69
  }>;
70
+ /* Excluded from this release type: handleExternalLogout */
70
71
  /* Excluded from this release type: performRefresh */
71
72
  private setSession;
72
73
  private clearSession;
@@ -613,17 +614,28 @@ declare class TokenManager {
613
614
  private accessToken;
614
615
  private refreshToken;
615
616
  private refreshTimer;
617
+ private refreshInFlight;
616
618
  private storageKey;
617
619
  private onRefreshNeeded;
620
+ private onTokensChanged;
621
+ private boundHandleStorage;
618
622
  constructor(keyPrefix: string);
619
623
  setRefreshCallback(cb: () => Promise<void>): void;
624
+ setTokensChangedCallback(cb: () => void): void;
620
625
  getAccessToken(): string | null;
621
626
  getRefreshToken(): string | null;
622
627
  hasRefreshToken(): boolean;
623
628
  setTokens(accessToken: string, refreshToken: string): void;
624
629
  clearTokens(): void;
630
+ /**
631
+ * Coalesces concurrent refresh calls within this tab and coordinates
632
+ * across tabs via Web Locks API (when available).
633
+ */
634
+ refreshOnce(): Promise<void>;
625
635
  destroy(): void;
636
+ private executeRefresh;
626
637
  private scheduleRefresh;
638
+ private handleStorageEvent;
627
639
  private getTokenExpiry;
628
640
  private loadRefreshToken;
629
641
  private saveRefreshToken;
@@ -633,12 +645,16 @@ declare class TokenManager {
633
645
  declare class Transport {
634
646
  private baseUrl;
635
647
  private authMode;
648
+ private onUnauthorized;
636
649
  constructor(baseUrl: string, authMode: AuthMode);
650
+ /** Register a handler that refreshes tokens and returns a new access token, or null. */
651
+ setUnauthorizedHandler(handler: () => Promise<string | null>): void;
637
652
  request<T>(method: string, path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
638
653
  get<T>(path: string, headers?: Record<string, string>): Promise<T>;
639
654
  post<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
640
655
  patch<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
641
656
  del<T>(path: string, headers?: Record<string, string>): Promise<T>;
657
+ private doRequest;
642
658
  private getAuthHeaders;
643
659
  private inferDomain;
644
660
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saas-support/react",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "Unified embeddable SDK for SaaS Support — auth, billing, and reporting",
6
6
  "exports": {