@alter-ai/connect 0.9.1 → 0.10.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/alter-connect.cjs.js +1 -1
- package/dist/alter-connect.cjs.js.map +1 -1
- package/dist/alter-connect.esm.js +1 -1
- package/dist/alter-connect.esm.js.map +1 -1
- package/dist/alter-connect.umd.js +1 -1
- package/dist/alter-connect.umd.js.map +1 -1
- package/dist/types/core/alter-connect.d.ts +4 -3
- package/dist/types/core/alter-connect.d.ts.map +1 -1
- package/dist/types/core/config.d.ts +2 -2
- package/dist/types/core/config.d.ts.map +1 -1
- package/dist/types/core/events.d.ts +2 -3
- package/dist/types/core/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/oauth/handler.d.ts +1 -1
- package/dist/types/oauth/handler.d.ts.map +1 -1
- package/dist/types/state/manager.d.ts +1 -1
- package/dist/types/types/index.d.ts +5 -5
- package/dist/types/types/index.d.ts.map +1 -1
- package/dist/types/utils/mobile.d.ts +1 -1
- package/dist/types/utils/mobile.d.ts.map +1 -1
- package/package.json +7 -4
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});const e="https://backend.alterauth.com";function t(e,...t){e.debug&&console.log("[Alter Connect]",...t)}class s{constructor(){this.events=new Map}on(e,t){return this.events.has(e)||this.events.set(e,new Set),this.events.get(e).add(t),()=>this.off(e,t)}off(e,t){const s=this.events.get(e);s&&s.delete(t)}emit(e,...t){const s=this.events.get(e);s&&s.forEach(s=>{try{s(...t)}catch(t){console.error(`[Alter Connect] Error in event handler for '${e}':`,t)}})}removeAllListeners(e){e?this.events.delete(e):this.events.clear()}}class i{constructor(){this.state={isOpen:!1,sessionToken:null,error:null},this.listeners=new Set}getState(){return{...this.state}}get(e){return this.state[e]}setState(e){this.state={...this.state,...e},this.notifyListeners()}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}clearListeners(){this.listeners.clear()}notifyListeners(){const e=this.getState();this.listeners.forEach(t=>{try{t(e)}catch(e){console.error("[Alter Connect] Error in state listener:",e)}})}}function n(){const e=navigator.userAgent||navigator.vendor||window.opera,t=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(e),s="ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0,i=window.innerWidth<=768;return t||s&&i}function r(){return n()&&window.innerWidth<=480?"phone":n()&&window.innerWidth>480&&window.innerWidth<=1024?"tablet":"desktop"}function o(e){return"reauth"===e?"reauth":"creation"}function a(e){return"pending"===e?"pending":"error"===e?"error":"active"}function h(e){sessionStorage.removeItem("alter_oauth_state");try{const t=new URL(e);window.history.replaceState({},document.title,t.pathname+t.search+t.hash)}catch{window.history.replaceState({},document.title,window.location.pathname)}}class c{constructor(t){this.popup=null,this.pollInterval=null,this.messageListener=null,this.settled=!1,this.options={baseURL:t.baseURL,onSuccess:t.onSuccess,onError:t.onError,onCancel:t.onCancel,popupWidth:t.popupWidth||500,popupHeight:t.popupHeight||700,debug:t.debug||!1,expectedOrigin:t.expectedOrigin||""};try{this.expectedOrigin=new URL(t.baseURL).origin}catch{throw new Error(`Invalid baseURL: "${t.baseURL}". Must be a full URL with protocol (e.g., "${e}").`)}}startOAuth(e){if(!this.options.expectedOrigin)try{const t=new URL(e);this.options.expectedOrigin=t.origin,this.log("Derived expected origin:",this.options.expectedOrigin)}catch{return this.log("Failed to parse OAuth URL for origin validation:",e),void this.options.onError({code:"invalid_oauth_url",message:"Failed to determine origin from OAuth URL. Cannot proceed securely."})}!function(){const e=r();return"phone"===e||"tablet"===e&&window.innerHeight>window.innerWidth}()?(this.log("Using popup flow for desktop"),this.openPopup(e)):(this.log("Using redirect flow for mobile device"),this.startRedirectFlow(e))}startRedirectFlow(e){this.log("Starting redirect flow:",e);const t={timestamp:Date.now(),returnUrl:window.location.href};try{sessionStorage.setItem("alter_oauth_state",JSON.stringify(t))}catch(e){return this.log("Failed to save state:",e),void this.options.onError({code:"redirect_error",message:"Failed to start OAuth flow: could not save session state",details:{error:e}})}window.location.href=e}static checkOAuthReturn(e,t){const s=sessionStorage.getItem("alter_oauth_state");if(!s)return!1;try{const i=JSON.parse(s);if(Date.now()-i.timestamp>3e5)return sessionStorage.removeItem("alter_oauth_state"),!1;const n=new URLSearchParams(window.location.search),r=n.get("alter_connect_success"),c=n.get("alter_connect_error");if("true"===r){const s=n.get("grant_id"),r=n.get("provider"),c=n.get("account_identifier");if(!s||!r||!c)return h(i.returnUrl),t({code:"invalid_response",message:"OAuth redirect returned incomplete grant data"}),!0;const l={grant_id:s,provider:r,provider_name:n.get("provider_name")||r,account_identifier:c,timestamp:n.get("timestamp")||(new Date).toISOString(),operation:o(n.get("operation")),scopes:n.get("scopes")?.split(",")||[],status:a(n.get("status"))};return h(i.returnUrl),e([l]),!0}if(c){const e={code:n.get("error_code")||"oauth_error",message:n.get("error_description")||"OAuth authorization failed"};return h(i.returnUrl),t(e),!0}return!1}catch(e){return console.error("[OAuth Handler] Failed to check OAuth return:",e),sessionStorage.removeItem("alter_oauth_state"),!1}}openPopup(e){const t=window.screenX+(window.outerWidth-this.options.popupWidth)/2,s=window.screenY+(window.outerHeight-this.options.popupHeight)/2,i=[`width=${this.options.popupWidth}`,`height=${this.options.popupHeight}`,`left=${t}`,`top=${s}`,"resizable=yes","scrollbars=yes","status=yes"].join(",");this.log("Opening OAuth popup:",e),this.popup=window.open(e,"alter_oauth_popup",i),this.popup?(this.startPolling(),this.setupMessageListener()):this.options.onError({code:"popup_blocked",message:"Popup was blocked by browser. Please allow popups for this site."})}close(){this.log("Closing OAuth handler"),this.popup&&!this.popup.closed&&this.popup.close(),this.popup=null,null!==this.pollInterval&&(clearInterval(this.pollInterval),this.pollInterval=null),this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null)}startPolling(){this.pollInterval=window.setInterval(()=>{this.settled||this.popup&&!this.popup.closed||(this.log("Popup closed by user"),this.settled=!0,this.close(),this.options.onCancel())},500)}setupMessageListener(){this.messageListener=e=>{if(this.settled)return;if(e.origin!==this.expectedOrigin)return void this.log("Rejected message from unexpected origin:",e.origin,"(expected:",this.expectedOrigin+")");this.log("Received message from",e.origin);const t=e.data;if(t&&"object"==typeof t)if("alter_connect_success"===t.type){this.log("OAuth success");const e=Array.isArray(t.grants)?t.grants:null;if(e){this.log("Multi-provider success:",e.length,"grants");const t=[];for(const s of e){const e=s.grant_id;e&&s.provider?t.push({grant_id:e,provider:s.provider,provider_name:s.provider_name||s.provider,account_identifier:s.account_identifier||"",timestamp:s.timestamp||(new Date).toISOString(),operation:o(s.operation),scopes:Array.isArray(s.scopes)?s.scopes:[],status:a(s.status),metadata:s.metadata}):this.log("Skipping invalid grant item:",s)}return 0===t.length?(this.settled=!0,this.close(),void this.options.onError({code:"invalid_response",message:"Server returned empty grants array"})):(this.settled=!0,this.close(),void this.options.onSuccess(t))}this.settled=!0,this.close(),this.options.onError({code:"invalid_response",message:"Server returned success without grants array"})}else if("alter_connect_error"===t.type){this.log("OAuth error");const e={code:t.error||"oauth_error",message:t.error_description||"OAuth authorization failed",details:t};this.settled=!0,this.close(),this.options.onError(e)}},window.addEventListener("message",this.messageListener)}log(...e){this.options.debug&&console.log("[OAuth Handler]",...e)}}const l="0.2.0";let d=!1;class u{constructor(n={}){if(this._oauthHandler=null,this._perOpenCleanups=[],function(e){if(void 0!==e.baseURL)throw e.baseURL,new Error("AlterConnectConfig.baseURL is reserved and not yet supported. Omit the field to use the production Alter host.")}(n),this.config=function(e){return{debug:e.debug??!1}}(n),this._baseURL=(n.baseURL??e).replace(/\/+$/,""),this._baseURL!==e&&!d){d=!0;const e=new URL(this._baseURL).origin;console.warn(`[alter-connect] Using non-default Alter host: ${e}. Unset baseURL in AlterConnect.create() for production.`)}this.eventEmitter=new s,this.stateManager=new i,this._isInitialized=!0,t(this.config,"Alter Connect SDK initialized",{version:l,baseURL:this._baseURL}),this.checkRedirectReturn()}checkRedirectReturn(){c.checkOAuthReturn(e=>{t(this.config,"OAuth redirect return - success:",e),this.eventEmitter.emit("success",e)},e=>{t(this.config,"OAuth redirect return - error:",e),this.eventEmitter.emit("error",e)})&&t(this.config,"OAuth redirect return detected and handled")}static create(e){return new u(e)}async open(e){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call open() - SDK instance has been destroyed");if(t(this.config,"Opening Connect UI"),!e.token||"string"!=typeof e.token)throw this.createError("invalid_options","Session token is required. Create one from your backend using POST /sdk/oauth/connect/session");if(!e.onSuccess||"function"!=typeof e.onSuccess)throw this.createError("invalid_options","onSuccess callback is required");if(this.isOpen())return void t(this.config,"Connect UI is already open");this._oauthHandler=new c({baseURL:this._baseURL,onSuccess:e=>{t(this.config,"OAuth success:",e),this.handleOAuthSuccess(e)},onError:e=>{t(this.config,"OAuth error:",e),this.handleOAuthError(e)},onCancel:()=>{t(this.config,"OAuth cancelled (popup closed)"),this.handleOAuthCancel()},popupWidth:500,popupHeight:700,debug:this.config.debug}),this.stateManager.setState({isOpen:!0,error:null,sessionToken:e.token}),this.registerEventHandlers(e);const s=`${this._baseURL}/sdk/oauth/connect#session=${encodeURIComponent(e.token)}`;t(this.config,"Connect URL:",s),this._oauthHandler.startOAuth(s),e.onEvent&&e.onEvent("connect_opened",{timestamp:(new Date).toISOString()}),t(this.config,"Connect UI opened successfully")}close(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call close() - SDK instance has been destroyed");t(this.config,"Closing Connect UI"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this._perOpenCleanups.forEach(e=>e()),this._perOpenCleanups=[],this.stateManager.setState({isOpen:!1}),this.eventEmitter.emit("close")}destroy(){this._isInitialized&&(t(this.config,"Destroying SDK instance"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this.stateManager.get("isOpen")&&this.stateManager.setState({isOpen:!1}),this.eventEmitter.removeAllListeners(),this.stateManager.clearListeners(),this._isInitialized=!1)}on(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call on() - SDK instance has been destroyed");return this.eventEmitter.on(e,t)}off(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call off() - SDK instance has been destroyed");this.eventEmitter.off(e,t)}isOpen(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call isOpen() - SDK instance has been destroyed");return this.stateManager.get("isOpen")}getVersion(){return l}cleanupHandler(){this._oauthHandler=null}handleOAuthSuccess(e){this.cleanupHandler(),this.eventEmitter.emit("success",e)}handleOAuthError(e){this.cleanupHandler(),this.stateManager.setState({error:e}),this.eventEmitter.emit("error",e)}handleOAuthCancel(){this.cleanupHandler(),this.eventEmitter.emit("exit")}registerEventHandlers(e){e.onSuccess&&this._perOpenCleanups.push(this.eventEmitter.on("success",t=>{e.onSuccess(t),this.close()})),e.onExit&&this._perOpenCleanups.push(this.eventEmitter.on("exit",()=>{e.onExit(),this.close()})),this._perOpenCleanups.push(this.eventEmitter.on("error",t=>{e.onError&&e.onError(t),this.close()})),e.onEvent&&this._perOpenCleanups.push(this.eventEmitter.on("event",(t,s)=>{e.onEvent(t,s)}))}createError(e,t,s){const i=new Error(t);return i.code=e,i.details=s,i}}exports.default=u;
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});const e="https://backend.alterauth.com";function t(e,...t){e.debug&&console.log("[Alter Connect]",...t)}class s{constructor(){this.events=new Map}on(e,t){return this.events.has(e)||this.events.set(e,new Set),this.events.get(e).add(t),()=>this.off(e,t)}off(e,t){const s=this.events.get(e);s&&s.delete(t)}emit(e,...t){const s=this.events.get(e);s&&s.forEach(s=>{try{s(...t)}catch(t){console.error(`[Alter Connect] Error in event handler for '${e}':`,t)}})}removeAllListeners(e){e?this.events.delete(e):this.events.clear()}}class i{constructor(){this.state={isOpen:!1,sessionToken:null,error:null},this.listeners=new Set}getState(){return{...this.state}}get(e){return this.state[e]}setState(e){this.state={...this.state,...e},this.notifyListeners()}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}clearListeners(){this.listeners.clear()}notifyListeners(){const e=this.getState();this.listeners.forEach(t=>{try{t(e)}catch(e){console.error("[Alter Connect] Error in state listener:",e)}})}}function n(){const e=navigator.userAgent||navigator.vendor||window.opera||"",t=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(e),s="ontouchstart"in window||navigator.maxTouchPoints>0||(navigator.msMaxTouchPoints??0)>0,i=window.innerWidth<=768;return t||s&&i}function r(){return n()&&window.innerWidth<=480?"phone":n()&&window.innerWidth>480&&window.innerWidth<=1024?"tablet":"desktop"}function o(e){return"reauth"===e?"reauth":"creation"}function a(e){return"pending"===e?"pending":"error"===e?"error":"active"}function h(e){sessionStorage.removeItem("alter_oauth_state");try{const t=new URL(e);window.history.replaceState({},document.title,t.pathname+t.search+t.hash)}catch{window.history.replaceState({},document.title,window.location.pathname)}}class c{constructor(t){this.popup=null,this.pollInterval=null,this.messageListener=null,this.settled=!1,this.options={baseURL:t.baseURL,onSuccess:t.onSuccess,onError:t.onError,onCancel:t.onCancel,popupWidth:t.popupWidth||500,popupHeight:t.popupHeight||700,debug:t.debug||!1,expectedOrigin:t.expectedOrigin||""};try{this.expectedOrigin=new URL(t.baseURL).origin}catch{throw new Error(`Invalid baseURL: "${t.baseURL}". Must be a full URL with protocol (e.g., "${e}").`)}}startOAuth(e){if(!this.options.expectedOrigin)try{const t=new URL(e);this.options.expectedOrigin=t.origin,this.log("Derived expected origin:",this.options.expectedOrigin)}catch{return this.log("Failed to parse OAuth URL for origin validation:",e),void this.options.onError({code:"invalid_oauth_url",message:"Failed to determine origin from OAuth URL. Cannot proceed securely."})}!function(){const e=r();return"phone"===e||"tablet"===e&&window.innerHeight>window.innerWidth}()?(this.log("Using popup flow for desktop"),this.openPopup(e)):(this.log("Using redirect flow for mobile device"),this.startRedirectFlow(e))}startRedirectFlow(e){this.log("Starting redirect flow:",e);const t={timestamp:Date.now(),returnUrl:window.location.href};try{sessionStorage.setItem("alter_oauth_state",JSON.stringify(t))}catch(e){return this.log("Failed to save state:",e),void this.options.onError({code:"redirect_error",message:"Failed to start OAuth flow: could not save session state",details:{error:e}})}window.location.href=e}static checkOAuthReturn(e,t){const s=sessionStorage.getItem("alter_oauth_state");if(!s)return!1;try{const i=JSON.parse(s);if(Date.now()-i.timestamp>3e5)return sessionStorage.removeItem("alter_oauth_state"),!1;const n=new URLSearchParams(window.location.search),r=n.get("alter_connect_success"),c=n.get("alter_connect_error");if("true"===r){const s=n.get("grant_id"),r=n.get("provider"),c=n.get("account_identifier");if(!s||!r||!c)return h(i.returnUrl),t({code:"invalid_response",message:"OAuth redirect returned incomplete grant data"}),!0;const l={grant_id:s,provider:r,provider_name:n.get("provider_name")||r,account_identifier:c,timestamp:n.get("timestamp")||(new Date).toISOString(),operation:o(n.get("operation")),scopes:n.get("scopes")?.split(",")||[],status:a(n.get("status"))};return h(i.returnUrl),e([l]),!0}if(c){const e={code:n.get("error_code")||"oauth_error",message:n.get("error_description")||"OAuth authorization failed"};return h(i.returnUrl),t(e),!0}return!1}catch(e){return console.error("[OAuth Handler] Failed to check OAuth return:",e),sessionStorage.removeItem("alter_oauth_state"),!1}}openPopup(e){const t=window.screenX+(window.outerWidth-this.options.popupWidth)/2,s=window.screenY+(window.outerHeight-this.options.popupHeight)/2,i=[`width=${this.options.popupWidth}`,`height=${this.options.popupHeight}`,`left=${t}`,`top=${s}`,"resizable=yes","scrollbars=yes","status=yes"].join(",");this.log("Opening OAuth popup:",e),this.popup=window.open(e,"alter_oauth_popup",i),this.popup?(this.startPolling(),this.setupMessageListener()):this.options.onError({code:"popup_blocked",message:"Popup was blocked by browser. Please allow popups for this site."})}close(){this.log("Closing OAuth handler"),this.popup&&!this.popup.closed&&this.popup.close(),this.popup=null,null!==this.pollInterval&&(clearInterval(this.pollInterval),this.pollInterval=null),this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null)}startPolling(){this.pollInterval=window.setInterval(()=>{this.settled||this.popup&&!this.popup.closed||(this.log("Popup closed by user"),this.settled=!0,this.close(),this.options.onCancel())},500)}setupMessageListener(){this.messageListener=e=>{if(this.settled)return;if(e.origin!==this.expectedOrigin)return void this.log("Rejected message from unexpected origin:",e.origin,"(expected:",this.expectedOrigin+")");this.log("Received message from",e.origin);const t=e.data;if(t&&"object"==typeof t)if("alter_connect_success"===t.type){this.log("OAuth success");const e=Array.isArray(t.grants)?t.grants:null;if(e){this.log("Multi-provider success:",e.length,"grants");const t=[];for(const s of e){const e=s.grant_id;e&&s.provider?t.push({grant_id:e,provider:s.provider,provider_name:s.provider_name||s.provider,account_identifier:s.account_identifier||"",timestamp:s.timestamp||(new Date).toISOString(),operation:o(s.operation),scopes:Array.isArray(s.scopes)?s.scopes:[],status:a(s.status),metadata:s.metadata}):this.log("Skipping invalid grant item:",s)}return 0===t.length?(this.settled=!0,this.close(),void this.options.onError({code:"invalid_response",message:"Server returned empty grants array"})):(this.settled=!0,this.close(),void this.options.onSuccess(t))}this.settled=!0,this.close(),this.options.onError({code:"invalid_response",message:"Server returned success without grants array"})}else if("alter_connect_error"===t.type){this.log("OAuth error");const e={code:t.error||"oauth_error",message:t.error_description||"OAuth authorization failed",details:t};this.settled=!0,this.close(),this.options.onError(e)}},window.addEventListener("message",this.messageListener)}log(...e){this.options.debug&&console.log("[OAuth Handler]",...e)}}const l="0.2.0";let d=!1;class u{constructor(n={}){if(this._oauthHandler=null,this._perOpenCleanups=[],function(e){if(void 0!==e.baseURL)throw e.baseURL,new Error("AlterConnectConfig.baseURL is reserved and not yet supported. Omit the field to use the production Alter host.")}(n),this.config=function(e){return{debug:e.debug??!1}}(n),this._baseURL=(n.baseURL??e).replace(/\/+$/,""),this._baseURL!==e&&!d){d=!0;const e=new URL(this._baseURL).origin;console.warn(`[alter-connect] Using non-default Alter host: ${e}. Unset baseURL in AlterConnect.create() for production.`)}this.eventEmitter=new s,this.stateManager=new i,this._isInitialized=!0,t(this.config,"Alter Connect SDK initialized",{version:l,baseURL:this._baseURL}),this.checkRedirectReturn()}checkRedirectReturn(){c.checkOAuthReturn(e=>{t(this.config,"OAuth redirect return - success:",e),this.eventEmitter.emit("success",e)},e=>{t(this.config,"OAuth redirect return - error:",e),this.eventEmitter.emit("error",e)})&&t(this.config,"OAuth redirect return detected and handled")}static create(e){return new u(e)}async open(e){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call open() - SDK instance has been destroyed");if(t(this.config,"Opening Connect UI"),!e.token||"string"!=typeof e.token)throw this.createError("invalid_options","Session token is required. Create one from your backend using POST /sdk/oauth/connect/session");if(!e.onSuccess||"function"!=typeof e.onSuccess)throw this.createError("invalid_options","onSuccess callback is required");if(this.isOpen())return void t(this.config,"Connect UI is already open");this._oauthHandler=new c({baseURL:this._baseURL,onSuccess:e=>{t(this.config,"OAuth success:",e),this.handleOAuthSuccess(e)},onError:e=>{t(this.config,"OAuth error:",e),this.handleOAuthError(e)},onCancel:()=>{t(this.config,"OAuth cancelled (popup closed)"),this.handleOAuthCancel()},popupWidth:500,popupHeight:700,debug:this.config.debug}),this.stateManager.setState({isOpen:!0,error:null,sessionToken:e.token}),this.registerEventHandlers(e);const s=`${this._baseURL}/sdk/oauth/connect#session=${encodeURIComponent(e.token)}`;t(this.config,"Connect URL:",s),this._oauthHandler.startOAuth(s),e.onEvent&&e.onEvent("connect_opened",{timestamp:(new Date).toISOString()}),t(this.config,"Connect UI opened successfully")}close(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call close() - SDK instance has been destroyed");t(this.config,"Closing Connect UI"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this._perOpenCleanups.forEach(e=>e()),this._perOpenCleanups=[],this.stateManager.setState({isOpen:!1}),this.eventEmitter.emit("close")}destroy(){this._isInitialized&&(t(this.config,"Destroying SDK instance"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this.stateManager.get("isOpen")&&this.stateManager.setState({isOpen:!1}),this.eventEmitter.removeAllListeners(),this.stateManager.clearListeners(),this._isInitialized=!1)}on(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call on() - SDK instance has been destroyed");return this.eventEmitter.on(e,t)}off(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call off() - SDK instance has been destroyed");this.eventEmitter.off(e,t)}isOpen(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call isOpen() - SDK instance has been destroyed");return this.stateManager.get("isOpen")}getVersion(){return l}cleanupHandler(){this._oauthHandler=null}handleOAuthSuccess(e){this.cleanupHandler(),this.eventEmitter.emit("success",e)}handleOAuthError(e){this.cleanupHandler(),this.stateManager.setState({error:e}),this.eventEmitter.emit("error",e)}handleOAuthCancel(){this.cleanupHandler(),this.eventEmitter.emit("exit")}registerEventHandlers(e){e.onSuccess&&this._perOpenCleanups.push(this.eventEmitter.on("success",t=>{e.onSuccess(t),this.close()})),e.onExit&&this._perOpenCleanups.push(this.eventEmitter.on("exit",()=>{e.onExit(),this.close()})),this._perOpenCleanups.push(this.eventEmitter.on("error",t=>{e.onError&&e.onError(t),this.close()})),e.onEvent&&this._perOpenCleanups.push(this.eventEmitter.on("event",(t,s)=>{e.onEvent(t,s)}))}createError(e,t,s){const i=new Error(t);return i.code=e,i.details=s,i}}exports.default=u;
|
|
2
2
|
//# sourceMappingURL=alter-connect.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"alter-connect.cjs.js","sources":["../src/core/config.ts","../src/core/events.ts","../src/state/manager.ts","../src/utils/mobile.ts","../src/oauth/handler.ts","../src/core/alter-connect.ts"],"sourcesContent":["/**\n * Configuration validation and management\n */\n\nimport type { AlterConnectConfig } from '../types';\n\n/**\n * Canonical Alter Connect host.\n *\n * Single source of truth for the widget's default host. `alter-connect.ts`\n * uses it as the constructor fallback; `config.ts` and `oauth/handler.ts`\n * surface it in error messages so operators always see the same example\n * URL no matter which validation layer flags their misconfig.\n */\nexport const DEFAULT_ALTER_HOST = 'https://backend.alterauth.com';\n\n/**\n * Internal config used by the SDK\n */\ninterface FullConfig {\n debug: boolean;\n}\n\n/**\n * Validate configuration.\n *\n * `baseURL` is RESERVED in the public type for future self-hosted /\n * non-production support. It is not yet accepted at runtime — see the\n * `REMOVE_TO_ENABLE_BASE_URL_OVERRIDE` block below. The deep validation\n * beneath it is intentionally kept so re-enabling is a single-line\n * deletion; each check retains its inline rationale so future-readers\n * don't have to reconstruct the reasoning.\n */\nconst ALLOWED_BASE_URL_PROTOCOLS = ['http:', 'https:'];\n\nexport function validateConfig(config: AlterConnectConfig): void {\n if (config.baseURL !== undefined) {\n // Capture into a locally-typed const so the unreachable block below\n // still type-checks after the early throw (TS loses the narrowing\n // from `config.baseURL !== undefined` across the throw boundary).\n const baseURL: string = config.baseURL;\n\n // --- REMOVE_TO_ENABLE_BASE_URL_OVERRIDE ---\n // Delete this throw to enable baseURL override support. The deep\n // validation below is ready to accept any well-formed http/https\n // URL without userinfo, query, or fragment. Keep the throw message\n // free of the supplied value — callers may pass strings carrying\n // secrets, and thrown errors flow into error-tracking integrations.\n throw new Error(\n 'AlterConnectConfig.baseURL is reserved and not yet supported. Omit the field to use the production Alter host.'\n );\n // --- END REMOVE_TO_ENABLE_BASE_URL_OVERRIDE ---\n\n // Everything below is unreachable today (guarded by the throw above)\n // but preserved so re-enabling the override is a one-line change.\n // `eslint-disable-next-line` only silences the one following\n // statement, so every subsequent unreachable statement in this\n // block would re-trigger `no-unreachable`. Wrap the whole block\n // with an enable/disable pair instead.\n /* eslint-disable no-unreachable */\n if (typeof baseURL !== 'string' || baseURL.trim() === '') {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: must be a non-empty string. Omit the field to use the default Alter host.'\n );\n }\n if (baseURL !== baseURL.trim()) {\n // URL parsers silently trim ASCII whitespace but we store the raw\n // string on the instance and string-concat it into popup URLs, so\n // a padded value produces malformed popups. Reject loudly instead.\n //\n // Intentionally do NOT echo the offending value — a misconfigured\n // string could sit next to secret material, and thrown errors flow\n // into error-tracking integrations (Sentry, Datadog RUM, etc.).\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: contains leading or trailing whitespace.'\n );\n }\n let parsed: URL;\n try {\n parsed = new URL(baseURL);\n } catch {\n // Do NOT echo baseURL — a malformed string can still carry secret\n // material (e.g. query tokens that slipped past the whitespace\n // check) that would flow into error trackers via the thrown\n // message. The error tells the operator which rule was violated;\n // they can inspect their own config to debug.\n throw new Error(\n `Invalid AlterConnectConfig.baseURL: must be a full URL with protocol (e.g., \"${DEFAULT_ALTER_HOST}\").`\n );\n }\n // Reject every field that could carry secret material BEFORE any\n // downstream code echoes the URL string. userinfo = inline basic-auth\n // credentials, search = query-string tokens, hash = session tokens.\n // A base URL is just a host (+ optional path prefix) by definition —\n // if the operator needs any of these, they're passing the wrong value.\n if (parsed.username !== '' || parsed.password !== '') {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: credentials embedded in the URL (user:password@) are not allowed. Remove them from the URL and provide credentials through the normal auth flow.'\n );\n }\n if (parsed.search !== '') {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: query strings are not allowed on a base URL. Remove the \"?...\" portion.'\n );\n }\n if (parsed.hash !== '') {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: URL fragments are not allowed on a base URL. Remove the \"#...\" portion.'\n );\n }\n if (!ALLOWED_BASE_URL_PROTOCOLS.includes(parsed.protocol)) {\n // Do NOT echo baseURL — at this point we've rejected userinfo,\n // query, and fragment, so the remaining secret-leak risk is low,\n // but echoing is inconsistent with the other validation branches\n // and isn't needed to convey the rule to the operator.\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: only http:// and https:// are allowed.'\n );\n }\n /* eslint-enable no-unreachable */\n }\n}\n\n/**\n * Merge user config with defaults\n */\nexport function mergeConfig(config: AlterConnectConfig): FullConfig {\n return {\n debug: config.debug ?? false,\n };\n}\n\n/**\n * Debug logging helper\n */\nexport function debugLog(config: FullConfig, ...args: any[]): void {\n if (config.debug) {\n console.log('[Alter Connect]', ...args);\n }\n}\n\n/**\n * Export FullConfig type for use in other modules\n */\nexport type { FullConfig };\n","/**\n * Simple event emitter for SDK events\n */\n\ntype EventHandler = (...args: any[]) => void;\n\n/**\n * Event emitter for SDK internal events\n */\nexport class EventEmitter {\n private events: Map<string, Set<EventHandler>>;\n\n constructor() {\n this.events = new Map();\n }\n\n /**\n * Register an event listener\n * @returns Unsubscribe function\n */\n on(event: string, handler: EventHandler): () => void {\n if (!this.events.has(event)) {\n this.events.set(event, new Set());\n }\n\n this.events.get(event)!.add(handler);\n\n // Return unsubscribe function\n return () => this.off(event, handler);\n }\n\n /**\n * Remove an event listener\n */\n off(event: string, handler: EventHandler): void {\n const handlers = this.events.get(event);\n if (handlers) {\n handlers.delete(handler);\n }\n }\n\n /**\n * Emit an event\n */\n emit(event: string, ...args: any[]): void {\n const handlers = this.events.get(event);\n if (handlers) {\n handlers.forEach(handler => {\n try {\n handler(...args);\n } catch (error) {\n console.error(`[Alter Connect] Error in event handler for '${event}':`, error);\n }\n });\n }\n }\n\n /**\n * Remove all listeners for an event, or all listeners if no event specified\n */\n removeAllListeners(event?: string): void {\n if (event) {\n this.events.delete(event);\n } else {\n this.events.clear();\n }\n }\n}\n","/**\n * State management for SDK\n */\n\nimport type { AlterError } from '../types';\n\n/**\n * SDK internal state\n */\nexport interface SDKState {\n isOpen: boolean;\n sessionToken: string | null;\n error: AlterError | null;\n}\n\ntype StateListener = (state: SDKState) => void;\n\n/**\n * Simple state manager\n */\nexport class StateManager {\n private state: SDKState;\n private listeners: Set<StateListener>;\n\n constructor() {\n this.state = {\n isOpen: false,\n sessionToken: null,\n error: null,\n };\n this.listeners = new Set();\n }\n\n /**\n * Get current state\n */\n getState(): Readonly<SDKState> {\n return { ...this.state };\n }\n\n /**\n * Get a specific state value\n */\n get<K extends keyof SDKState>(key: K): SDKState[K] {\n return this.state[key];\n }\n\n /**\n * Update state\n */\n setState(updates: Partial<SDKState>): void {\n this.state = { ...this.state, ...updates };\n this.notifyListeners();\n }\n\n /**\n * Subscribe to state changes\n */\n subscribe(listener: StateListener): () => void {\n this.listeners.add(listener);\n\n // Return unsubscribe function\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Clear all listeners\n */\n clearListeners(): void {\n this.listeners.clear();\n }\n\n /**\n * Notify all listeners of state change\n */\n private notifyListeners(): void {\n const currentState = this.getState();\n this.listeners.forEach(listener => {\n try {\n listener(currentState);\n } catch (error) {\n console.error('[Alter Connect] Error in state listener:', error);\n }\n });\n }\n}\n","/**\n * Mobile device detection and responsive utilities\n */\n\n/**\n * Detect if the current device is a mobile device\n */\nexport function isMobileDevice(): boolean {\n const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;\n\n // Check for mobile patterns\n const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;\n const isMobileUA = mobileRegex.test(userAgent);\n\n // Check for touch capability\n const hasTouchScreen = ('ontouchstart' in window) ||\n (navigator.maxTouchPoints > 0) ||\n ((navigator as any).msMaxTouchPoints > 0);\n\n // Check screen size\n const isSmallScreen = window.innerWidth <= 768;\n\n return isMobileUA || (hasTouchScreen && isSmallScreen);\n}\n\n/**\n * Detect if the device is specifically a phone (vs tablet)\n */\nexport function isPhoneDevice(): boolean {\n return isMobileDevice() && window.innerWidth <= 480;\n}\n\n/**\n * Detect if the device is a tablet\n */\nexport function isTabletDevice(): boolean {\n return isMobileDevice() && window.innerWidth > 480 && window.innerWidth <= 1024;\n}\n\n/**\n * Get the device type\n */\nexport function getDeviceType(): 'desktop' | 'tablet' | 'phone' {\n if (isPhoneDevice()) return 'phone';\n if (isTabletDevice()) return 'tablet';\n return 'desktop';\n}\n\n/**\n * Check if we should use redirect flow instead of popup\n *\n * Popup windows don't work well on mobile:\n * - Browsers often block them\n * - They open as full-screen tabs\n * - Users lose context of parent app\n */\nexport function shouldUseRedirectFlow(): boolean {\n const deviceType = getDeviceType();\n\n // Always use redirect for phones\n if (deviceType === 'phone') return true;\n\n // Use redirect for tablets in portrait mode (popups are cramped)\n if (deviceType === 'tablet' && window.innerHeight > window.innerWidth) {\n return true;\n }\n\n // Use popup for desktop and tablets in landscape\n return false;\n}\n","/**\n * OAuth popup and redirect handler\n */\n\nimport type { Grant, AlterError } from '../types';\nimport { DEFAULT_ALTER_HOST } from '../core/config';\nimport { shouldUseRedirectFlow } from '../utils/mobile';\n\ninterface OAuthHandlerOptions {\n baseURL: string;\n onSuccess: (grants: Grant[]) => void;\n onError: (error: AlterError) => void;\n onCancel: () => void;\n popupWidth?: number;\n popupHeight?: number;\n debug?: boolean;\n /** Expected origin for postMessage validation. Derived from the OAuth URL. */\n expectedOrigin?: string;\n}\n\n/**\n * Parse an operation string into a valid Grant['operation'] value\n */\nfunction parseOperation(value: string | null | undefined): Grant['operation'] {\n if (value === 'reauth') return 'reauth';\n return 'creation';\n}\n\n/**\n * Parse a status string into a valid Grant['status'] value\n */\nfunction parseStatus(value: string | null | undefined): Grant['status'] {\n if (value === 'pending') return 'pending';\n if (value === 'error') return 'error';\n return 'active';\n}\n\n/**\n * Clean up OAuth redirect state and restore the original URL\n */\nfunction cleanupRedirectState(returnUrl: string): void {\n sessionStorage.removeItem('alter_oauth_state');\n try {\n const cleanUrl = new URL(returnUrl);\n window.history.replaceState({}, document.title, cleanUrl.pathname + cleanUrl.search + cleanUrl.hash);\n } catch {\n // If returnUrl is malformed, just remove query params from current URL\n window.history.replaceState({}, document.title, window.location.pathname);\n }\n}\n\n/**\n * Handles OAuth flow in popup window\n */\nexport class OAuthHandler {\n private popup: Window | null = null;\n private pollInterval: number | null = null;\n private messageListener: ((event: MessageEvent) => void) | null = null;\n private options: Required<OAuthHandlerOptions>;\n private expectedOrigin: string;\n private settled: boolean = false;\n\n constructor(options: OAuthHandlerOptions) {\n this.options = {\n baseURL: options.baseURL,\n onSuccess: options.onSuccess,\n onError: options.onError,\n onCancel: options.onCancel,\n popupWidth: options.popupWidth || 500,\n popupHeight: options.popupHeight || 700,\n debug: options.debug || false,\n expectedOrigin: options.expectedOrigin || '',\n };\n\n // Extract origin from baseURL for postMessage validation\n try {\n this.expectedOrigin = new URL(options.baseURL).origin;\n } catch {\n throw new Error(\n `Invalid baseURL: \"${options.baseURL}\". Must be a full URL with protocol (e.g., \"${DEFAULT_ALTER_HOST}\").`\n );\n }\n }\n\n /**\n * Start OAuth flow (automatically chooses popup or redirect based on device)\n */\n startOAuth(url: string): void {\n // Derive expected origin from the OAuth URL if not explicitly set.\n // SECURITY: Fail-closed — if we can't determine origin, abort the flow.\n if (!this.options.expectedOrigin) {\n try {\n const parsed = new URL(url);\n this.options.expectedOrigin = parsed.origin;\n this.log('Derived expected origin:', this.options.expectedOrigin);\n } catch {\n this.log('Failed to parse OAuth URL for origin validation:', url);\n this.options.onError({\n code: 'invalid_oauth_url',\n message: 'Failed to determine origin from OAuth URL. Cannot proceed securely.',\n });\n return;\n }\n }\n\n if (shouldUseRedirectFlow()) {\n this.log('Using redirect flow for mobile device');\n this.startRedirectFlow(url);\n } else {\n this.log('Using popup flow for desktop');\n this.openPopup(url);\n }\n }\n\n /**\n * Start OAuth redirect flow (for mobile)\n * Saves state and redirects the entire page\n */\n private startRedirectFlow(url: string): void {\n this.log('Starting redirect flow:', url);\n\n // Save state to sessionStorage to restore after redirect\n const state = {\n timestamp: Date.now(),\n returnUrl: window.location.href,\n };\n\n try {\n sessionStorage.setItem('alter_oauth_state', JSON.stringify(state));\n } catch (error) {\n this.log('Failed to save state:', error);\n this.options.onError({\n code: 'redirect_error',\n message: 'Failed to start OAuth flow: could not save session state',\n details: { error },\n });\n return;\n }\n\n // Redirect to OAuth URL (full page redirect)\n window.location.href = url;\n }\n\n /**\n * Check if returning from OAuth redirect and handle result\n * Call this on page load to detect OAuth return\n *\n * @returns true if OAuth return was detected and handled\n */\n static checkOAuthReturn(\n onSuccess: (grants: Grant[]) => void,\n onError: (error: AlterError) => void\n ): boolean {\n // Check if we have OAuth state saved\n const stateJson = sessionStorage.getItem('alter_oauth_state');\n if (!stateJson) {\n return false;\n }\n\n try {\n const state = JSON.parse(stateJson);\n\n // Check if this is a valid OAuth return (within 5 minutes)\n const elapsed = Date.now() - state.timestamp;\n if (elapsed > 5 * 60 * 1000) {\n // Stale state, clean up\n sessionStorage.removeItem('alter_oauth_state');\n return false;\n }\n\n // Check URL for OAuth callback parameters\n const urlParams = new URLSearchParams(window.location.search);\n const success = urlParams.get('alter_connect_success');\n const error = urlParams.get('alter_connect_error');\n\n if (success === 'true') {\n // Validate required fields\n const grantId = urlParams.get('grant_id');\n const provider = urlParams.get('provider');\n const accountIdentifier = urlParams.get('account_identifier');\n\n if (!grantId || !provider || !accountIdentifier) {\n cleanupRedirectState(state.returnUrl);\n onError({\n code: 'invalid_response',\n message: 'OAuth redirect returned incomplete grant data',\n });\n return true;\n }\n\n const grant: Grant = {\n grant_id: grantId,\n provider: provider,\n provider_name: urlParams.get('provider_name') || provider,\n account_identifier: accountIdentifier,\n timestamp: urlParams.get('timestamp') || new Date().toISOString(),\n operation: parseOperation(urlParams.get('operation')),\n scopes: urlParams.get('scopes')?.split(',') || [],\n status: parseStatus(urlParams.get('status')),\n };\n\n cleanupRedirectState(state.returnUrl);\n onSuccess([grant]); // Wrap in array for unified interface\n return true;\n }\n\n if (error) {\n const alterError: AlterError = {\n code: urlParams.get('error_code') || 'oauth_error',\n message: urlParams.get('error_description') || 'OAuth authorization failed',\n };\n\n cleanupRedirectState(state.returnUrl);\n onError(alterError);\n return true;\n }\n\n return false;\n } catch (err) {\n console.error('[OAuth Handler] Failed to check OAuth return:', err);\n sessionStorage.removeItem('alter_oauth_state');\n return false;\n }\n }\n\n /**\n * Open OAuth popup (for desktop)\n */\n openPopup(url: string): void {\n // Calculate popup position (centered)\n const left = window.screenX + (window.outerWidth - this.options.popupWidth) / 2;\n const top = window.screenY + (window.outerHeight - this.options.popupHeight) / 2;\n\n const features = [\n `width=${this.options.popupWidth}`,\n `height=${this.options.popupHeight}`,\n `left=${left}`,\n `top=${top}`,\n 'resizable=yes',\n 'scrollbars=yes',\n 'status=yes',\n ].join(',');\n\n this.log('Opening OAuth popup:', url);\n\n // Open popup\n this.popup = window.open(url, 'alter_oauth_popup', features);\n\n if (!this.popup) {\n this.options.onError({\n code: 'popup_blocked',\n message: 'Popup was blocked by browser. Please allow popups for this site.',\n });\n return;\n }\n\n // Start monitoring popup\n this.startPolling();\n this.setupMessageListener();\n }\n\n /**\n * Close popup and clean up\n */\n close(): void {\n this.log('Closing OAuth handler');\n\n // Close popup\n if (this.popup && !this.popup.closed) {\n this.popup.close();\n }\n this.popup = null;\n\n // Stop polling\n if (this.pollInterval !== null) {\n clearInterval(this.pollInterval);\n this.pollInterval = null;\n }\n\n // Remove message listener\n if (this.messageListener) {\n window.removeEventListener('message', this.messageListener);\n this.messageListener = null;\n }\n }\n\n /**\n * Poll to detect when popup is closed\n */\n private startPolling(): void {\n this.pollInterval = window.setInterval(() => {\n if (this.settled) return;\n if (!this.popup || this.popup.closed) {\n this.log('Popup closed by user');\n this.settled = true;\n this.close();\n this.options.onCancel();\n }\n }, 500);\n }\n\n /**\n * Listen for postMessage from OAuth callback page\n */\n private setupMessageListener(): void {\n this.messageListener = (event: MessageEvent) => {\n // Ignore if already settled (prevents race with polling)\n if (this.settled) return;\n\n // Validate origin against the expected backend origin\n if (event.origin !== this.expectedOrigin) {\n this.log('Rejected message from unexpected origin:', event.origin, '(expected:', this.expectedOrigin + ')');\n return;\n }\n\n this.log('Received message from', event.origin);\n\n const data = event.data;\n\n // Check message type\n if (data && typeof data === 'object') {\n if (data.type === 'alter_connect_success') {\n this.log('OAuth success');\n\n // Accept both grants array and legacy connections array\n const rawGrants = Array.isArray(data.grants) ? data.grants : null;\n if (rawGrants) {\n this.log('Multi-provider success:', rawGrants.length, 'grants');\n const grants: Grant[] = [];\n for (const item of rawGrants) {\n const grantId = item.grant_id as string | undefined;\n if (!grantId || !item.provider) {\n this.log('Skipping invalid grant item:', item);\n continue;\n }\n grants.push({\n grant_id: grantId,\n provider: item.provider as string,\n provider_name: (item.provider_name as string) || (item.provider as string),\n account_identifier: (item.account_identifier as string) || '',\n timestamp: (item.timestamp as string) || new Date().toISOString(),\n operation: parseOperation(item.operation as string | undefined),\n scopes: Array.isArray(item.scopes) ? item.scopes as string[] : [],\n status: parseStatus(item.status as string | undefined),\n metadata: item.metadata as Grant['metadata'],\n });\n }\n\n if (grants.length === 0) {\n this.settled = true;\n this.close();\n this.options.onError({\n code: 'invalid_response',\n message: 'Server returned empty grants array',\n });\n return;\n }\n\n this.settled = true;\n this.close();\n this.options.onSuccess(grants);\n return;\n }\n\n // No valid grants array found\n this.settled = true;\n this.close();\n this.options.onError({\n code: 'invalid_response',\n message: 'Server returned success without grants array',\n });\n } else if (data.type === 'alter_connect_error') {\n this.log('OAuth error');\n\n const error: AlterError = {\n code: (data.error as string) || 'oauth_error',\n message: (data.error_description as string) || 'OAuth authorization failed',\n details: data as Record<string, unknown>,\n };\n\n this.settled = true;\n this.close();\n this.options.onError(error);\n }\n }\n };\n\n window.addEventListener('message', this.messageListener);\n }\n\n /**\n * Log debug message\n */\n private log(...args: unknown[]): void {\n if (this.options.debug) {\n console.log('[OAuth Handler]', ...args);\n }\n }\n}\n","/**\n * Main AlterConnect SDK class\n *\n * Opens the backend-served Connect UI in a popup. The backend handles\n * provider selection, branding, and OAuth — then sends the result back via postMessage.\n */\n\nimport type { AlterConnectConfig, OpenOptions, Grant, AlterError } from '../types';\nimport { validateConfig, mergeConfig, debugLog, DEFAULT_ALTER_HOST, type FullConfig } from './config';\nimport { EventEmitter } from './events';\nimport { StateManager } from '../state/manager';\nimport { OAuthHandler } from '../oauth/handler';\n\n/**\n * SDK version\n */\nconst VERSION = '0.2.0';\n\n/**\n * Module-scoped flag so the non-default-host warning fires at most once\n * per process. Reset is intentionally not exposed — the warning exists to\n * surface accidental misconfigurations during dev, not to spam logs.\n */\nlet hasWarnedNonDefaultHost = false;\n\n/**\n * Main SDK class\n */\nexport class AlterConnect {\n private config: FullConfig;\n private eventEmitter: EventEmitter;\n private stateManager: StateManager;\n private _isInitialized: boolean;\n private _oauthHandler: OAuthHandler | null = null;\n private readonly _baseURL: string;\n private _perOpenCleanups: Array<() => void> = [];\n\n /**\n * Private constructor (use AlterConnect.create() instead)\n */\n private constructor(config: AlterConnectConfig = {}) {\n // Validate and merge config\n validateConfig(config);\n this.config = mergeConfig(config);\n\n // Resolve the Alter host once at construction time. `baseURL` is an\n // environment-level setting (production vs self-hosted vs local-dev),\n // not a per-`open()` decision — that's why it lives on the constructor\n // config and not on OpenOptions.\n this._baseURL = (config.baseURL ?? DEFAULT_ALTER_HOST).replace(/\\/+$/, '');\n if (this._baseURL !== DEFAULT_ALTER_HOST && !hasWarnedNonDefaultHost) {\n hasWarnedNonDefaultHost = true;\n // Log only the origin, not the full URL. validateConfig rejects\n // userinfo/search/hash, so the path is the only \"extra\" piece the\n // origin strips — but it costs us nothing to be defensive here in\n // case validateConfig's allowlist is ever broadened. The warn\n // message is forwarded to customer-side error trackers (Sentry,\n // Datadog RUM), so minimizing what flows through it is prudent.\n const origin = new URL(this._baseURL).origin;\n console.warn(\n `[alter-connect] Using non-default Alter host: ${origin}. ` +\n 'Unset baseURL in AlterConnect.create() for production.'\n );\n }\n\n // Initialize components\n this.eventEmitter = new EventEmitter();\n this.stateManager = new StateManager();\n this._isInitialized = true;\n\n debugLog(this.config, 'Alter Connect SDK initialized', { version: VERSION, baseURL: this._baseURL });\n\n // Check if returning from OAuth redirect (mobile flow)\n this.checkRedirectReturn();\n }\n\n /**\n * Check if returning from OAuth redirect and handle result\n * This runs automatically on SDK initialization\n */\n private checkRedirectReturn(): void {\n const handled = OAuthHandler.checkOAuthReturn(\n (grants: Grant[]) => {\n debugLog(this.config, 'OAuth redirect return - success:', grants);\n // Emit success event (will be picked up by event handlers if SDK was re-initialized)\n this.eventEmitter.emit('success', grants);\n },\n (error: AlterError) => {\n debugLog(this.config, 'OAuth redirect return - error:', error);\n // Emit error event\n this.eventEmitter.emit('error', error);\n }\n );\n\n if (handled) {\n debugLog(this.config, 'OAuth redirect return detected and handled');\n }\n }\n\n /**\n * Create a new AlterConnect instance\n *\n * @param config Optional configuration\n * @returns AlterConnect instance\n *\n * @example\n * const alterConnect = AlterConnect.create();\n * const alterConnect = AlterConnect.create({ debug: true });\n */\n static create(config?: AlterConnectConfig): AlterConnect {\n return new AlterConnect(config);\n }\n\n /**\n * Open the Connect UI in a popup window\n *\n * Opens the backend-served Connect UI at {baseURL}/oauth/connect#session={token}.\n * The backend handles provider selection, branding, and OAuth flow.\n * On success, the backend sends a postMessage back to this window.\n *\n * @param options Configuration including the session token from your backend\n *\n * @example\n * // 1. Get session token from YOUR backend\n * const { session_token } = await fetch('/api/alter/create-session').then(r => r.json());\n *\n * // 2. Open Connect UI\n * await alterConnect.open({\n * token: session_token,\n * onSuccess: (grants) => {\n * console.log('Connected!', grants);\n * },\n * onError: (error) => {\n * console.error('Failed:', error);\n * }\n * });\n */\n async open(options: OpenOptions): Promise<void> {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call open() - SDK instance has been destroyed');\n }\n\n debugLog(this.config, 'Opening Connect UI');\n\n // Validate required options\n if (!options.token || typeof options.token !== 'string') {\n throw this.createError('invalid_options', 'Session token is required. Create one from your backend using POST /sdk/oauth/connect/session');\n }\n\n if (!options.onSuccess || typeof options.onSuccess !== 'function') {\n throw this.createError('invalid_options', 'onSuccess callback is required');\n }\n\n // Check if already open\n if (this.isOpen()) {\n debugLog(this.config, 'Connect UI is already open');\n return;\n }\n\n // Create OAuth handler first — this validates baseURL and throws on invalid input.\n // We do this BEFORE setting isOpen to avoid leaving the SDK in a broken state.\n this._oauthHandler = new OAuthHandler({\n baseURL: this._baseURL,\n onSuccess: (grants: Grant[]) => {\n debugLog(this.config, 'OAuth success:', grants);\n this.handleOAuthSuccess(grants);\n },\n onError: (error: AlterError) => {\n debugLog(this.config, 'OAuth error:', error);\n this.handleOAuthError(error);\n },\n onCancel: () => {\n debugLog(this.config, 'OAuth cancelled (popup closed)');\n this.handleOAuthCancel();\n },\n popupWidth: 500,\n popupHeight: 700,\n debug: this.config.debug,\n });\n\n // Update state (after handler creation succeeds)\n this.stateManager.setState({\n isOpen: true,\n error: null,\n sessionToken: options.token,\n });\n\n // Register event handlers\n this.registerEventHandlers(options);\n\n // Build the Connect UI URL — the backend serves the full UI\n const connectURL = `${this._baseURL}/sdk/oauth/connect#session=${encodeURIComponent(options.token)}`;\n\n debugLog(this.config, 'Connect URL:', connectURL);\n\n // Open the Connect UI (popup or redirect depending on device)\n this._oauthHandler.startOAuth(connectURL);\n\n // Emit analytics event\n if (options.onEvent) {\n options.onEvent('connect_opened', {\n timestamp: new Date().toISOString(),\n });\n }\n\n debugLog(this.config, 'Connect UI opened successfully');\n }\n\n /**\n * Close the Connect UI\n */\n close(): void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call close() - SDK instance has been destroyed');\n }\n\n debugLog(this.config, 'Closing Connect UI');\n\n // Clean up OAuth handler if active\n if (this._oauthHandler) {\n this._oauthHandler.close();\n this._oauthHandler = null;\n }\n\n // Remove only per-open handlers (preserves global handlers from on())\n this._perOpenCleanups.forEach(cleanup => cleanup());\n this._perOpenCleanups = [];\n\n // Update state\n this.stateManager.setState({ isOpen: false });\n\n this.eventEmitter.emit('close');\n }\n\n /**\n * Destroy the SDK instance and clean up resources\n */\n destroy(): void {\n if (!this._isInitialized) {\n // Already destroyed, silently return\n return;\n }\n\n debugLog(this.config, 'Destroying SDK instance');\n\n // Clean up OAuth handler if active\n if (this._oauthHandler) {\n this._oauthHandler.close();\n this._oauthHandler = null;\n }\n\n // Update state\n if (this.stateManager.get('isOpen')) {\n this.stateManager.setState({ isOpen: false });\n }\n\n // Clean up resources\n this.eventEmitter.removeAllListeners();\n this.stateManager.clearListeners();\n this._isInitialized = false;\n }\n\n /**\n * Register an event listener\n */\n on(event: string, handler: (...args: any[]) => void): () => void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call on() - SDK instance has been destroyed');\n }\n\n return this.eventEmitter.on(event, handler);\n }\n\n /**\n * Remove an event listener\n */\n off(event: string, handler: (...args: any[]) => void): void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call off() - SDK instance has been destroyed');\n }\n\n this.eventEmitter.off(event, handler);\n }\n\n /**\n * Check if Connect UI is open\n */\n isOpen(): boolean {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call isOpen() - SDK instance has been destroyed');\n }\n\n return this.stateManager.get('isOpen');\n }\n\n /**\n * Get SDK version\n */\n getVersion(): string {\n return VERSION;\n }\n\n /**\n * Clean up OAuth handler reference after a flow completes.\n * The OAuthHandler.close() is already called internally by the handler\n * before invoking callbacks, so we only need to null the reference here.\n */\n private cleanupHandler(): void {\n this._oauthHandler = null;\n }\n\n /**\n * Handle OAuth success\n */\n private handleOAuthSuccess(grants: Grant[]): void {\n this.cleanupHandler();\n this.eventEmitter.emit('success', grants);\n }\n\n /**\n * Handle OAuth error\n */\n private handleOAuthError(error: AlterError): void {\n this.cleanupHandler();\n this.stateManager.setState({ error });\n this.eventEmitter.emit('error', error);\n }\n\n /**\n * Handle OAuth cancellation (user closed popup)\n */\n private handleOAuthCancel(): void {\n this.cleanupHandler();\n this.eventEmitter.emit('exit');\n }\n\n /**\n * Register event handlers from OpenOptions\n */\n private registerEventHandlers(options: OpenOptions): void {\n // Success handler\n if (options.onSuccess) {\n this._perOpenCleanups.push(\n this.eventEmitter.on('success', (grants: Grant[]) => {\n options.onSuccess(grants);\n this.close();\n })\n );\n }\n\n // Exit handler\n if (options.onExit) {\n this._perOpenCleanups.push(\n this.eventEmitter.on('exit', () => {\n options.onExit!();\n this.close();\n })\n );\n }\n\n // Error handler — always registered to ensure close() is called\n this._perOpenCleanups.push(\n this.eventEmitter.on('error', (error: AlterError) => {\n if (options.onError) {\n options.onError(error);\n }\n this.close();\n })\n );\n\n // Event handler (analytics)\n if (options.onEvent) {\n this._perOpenCleanups.push(\n this.eventEmitter.on('event', (eventName: string, metadata: any) => {\n options.onEvent!(eventName, metadata);\n })\n );\n }\n }\n\n /**\n * Create an AlterError object\n */\n private createError(code: string, message: string, details?: Record<string, any>): Error {\n const error = new Error(message);\n (error as any).code = code;\n (error as any).details = details;\n return error;\n }\n}\n"],"names":["DEFAULT_ALTER_HOST","debugLog","config","args","debug","console","log","EventEmitter","constructor","this","events","Map","on","event","handler","has","set","Set","get","add","off","handlers","delete","emit","forEach","error","removeAllListeners","clear","StateManager","state","isOpen","sessionToken","listeners","getState","key","setState","updates","notifyListeners","subscribe","listener","clearListeners","currentState","isMobileDevice","userAgent","navigator","vendor","window","opera","isMobileUA","test","hasTouchScreen","maxTouchPoints","msMaxTouchPoints","isSmallScreen","innerWidth","getDeviceType","parseOperation","value","parseStatus","cleanupRedirectState","returnUrl","sessionStorage","removeItem","cleanUrl","URL","history","replaceState","document","title","pathname","search","hash","location","OAuthHandler","options","popup","pollInterval","messageListener","settled","baseURL","onSuccess","onError","onCancel","popupWidth","popupHeight","expectedOrigin","origin","Error","startOAuth","url","parsed","code","message","deviceType","innerHeight","shouldUseRedirectFlow","openPopup","startRedirectFlow","timestamp","Date","now","href","setItem","JSON","stringify","details","checkOAuthReturn","stateJson","getItem","parse","urlParams","URLSearchParams","success","grantId","provider","accountIdentifier","grant","grant_id","provider_name","account_identifier","toISOString","operation","scopes","split","status","alterError","err","left","screenX","outerWidth","top","screenY","outerHeight","features","join","open","startPolling","setupMessageListener","close","closed","clearInterval","removeEventListener","setInterval","data","type","rawGrants","Array","isArray","grants","length","item","push","metadata","error_description","addEventListener","VERSION","hasWarnedNonDefaultHost","AlterConnect","_oauthHandler","_perOpenCleanups","undefined","validateConfig","mergeConfig","_baseURL","replace","warn","eventEmitter","stateManager","_isInitialized","version","checkRedirectReturn","create","createError","token","handleOAuthSuccess","handleOAuthError","handleOAuthCancel","registerEventHandlers","connectURL","encodeURIComponent","onEvent","cleanup","destroy","getVersion","cleanupHandler","onExit","eventName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"alter-connect.cjs.js","sources":["../src/core/config.ts","../src/core/events.ts","../src/state/manager.ts","../src/utils/mobile.ts","../src/oauth/handler.ts","../src/core/alter-connect.ts"],"sourcesContent":["/**\n * Configuration validation and management\n */\n\nimport type { AlterConnectConfig } from \"../types\";\n\n/**\n * Canonical Alter Connect host.\n *\n * Single source of truth for the widget's default host. `alter-connect.ts`\n * uses it as the constructor fallback; `config.ts` and `oauth/handler.ts`\n * surface it in error messages so operators always see the same example\n * URL no matter which validation layer flags their misconfig.\n */\nexport const DEFAULT_ALTER_HOST = \"https://backend.alterauth.com\";\n\n/**\n * Internal config used by the SDK\n */\ninterface FullConfig {\n debug: boolean;\n}\n\n/**\n * Validate configuration.\n *\n * `baseURL` is RESERVED in the public type for future self-hosted /\n * non-production support. It is not yet accepted at runtime — see the\n * `REMOVE_TO_ENABLE_BASE_URL_OVERRIDE` block below. The deep validation\n * beneath it is intentionally kept so re-enabling is a single-line\n * deletion; each check retains its inline rationale so future-readers\n * don't have to reconstruct the reasoning.\n */\nconst ALLOWED_BASE_URL_PROTOCOLS = [\"http:\", \"https:\"];\n\nexport function validateConfig(config: AlterConnectConfig): void {\n if (config.baseURL !== undefined) {\n // Capture into a locally-typed const so the unreachable block below\n // still type-checks after the early throw (TS loses the narrowing\n // from `config.baseURL !== undefined` across the throw boundary).\n const baseURL: string = config.baseURL;\n\n // --- REMOVE_TO_ENABLE_BASE_URL_OVERRIDE ---\n // Delete this throw to enable baseURL override support. The deep\n // validation below is ready to accept any well-formed http/https\n // URL without userinfo, query, or fragment. Keep the throw message\n // free of the supplied value — callers may pass strings carrying\n // secrets, and thrown errors flow into error-tracking integrations.\n throw new Error(\n \"AlterConnectConfig.baseURL is reserved and not yet supported. Omit the field to use the production Alter host.\",\n );\n // --- END REMOVE_TO_ENABLE_BASE_URL_OVERRIDE ---\n\n // Everything below is unreachable today (guarded by the throw above)\n // but preserved so re-enabling the override is a one-line change.\n // This package enables `no-unreachable` (unreachable code is almost\n // always a bug), so this ONE deliberate case carries an explicit\n // disable/enable pair — deleting the throw above must also delete it.\n /* eslint-disable no-unreachable */\n if (typeof baseURL !== \"string\" || baseURL.trim() === \"\") {\n throw new Error(\n \"Invalid AlterConnectConfig.baseURL: must be a non-empty string. Omit the field to use the default Alter host.\",\n );\n }\n if (baseURL !== baseURL.trim()) {\n // URL parsers silently trim ASCII whitespace but we store the raw\n // string on the instance and string-concat it into popup URLs, so\n // a padded value produces malformed popups. Reject loudly instead.\n //\n // Intentionally do NOT echo the offending value — a misconfigured\n // string could sit next to secret material, and thrown errors flow\n // into error-tracking integrations (Sentry, Datadog RUM, etc.).\n throw new Error(\n \"Invalid AlterConnectConfig.baseURL: contains leading or trailing whitespace.\",\n );\n }\n let parsed: URL;\n try {\n parsed = new URL(baseURL);\n } catch {\n // Do NOT echo baseURL — a malformed string can still carry secret\n // material (e.g. query tokens that slipped past the whitespace\n // check) that would flow into error trackers via the thrown\n // message. The error tells the operator which rule was violated;\n // they can inspect their own config to debug.\n throw new Error(\n `Invalid AlterConnectConfig.baseURL: must be a full URL with protocol (e.g., \"${DEFAULT_ALTER_HOST}\").`,\n );\n }\n // Reject every field that could carry secret material BEFORE any\n // downstream code echoes the URL string. userinfo = inline basic-auth\n // credentials, search = query-string tokens, hash = session tokens.\n // A base URL is just a host (+ optional path prefix) by definition —\n // if the operator needs any of these, they're passing the wrong value.\n if (parsed.username !== \"\" || parsed.password !== \"\") {\n throw new Error(\n \"Invalid AlterConnectConfig.baseURL: credentials embedded in the URL (user:password@) are not allowed. Remove them from the URL and provide credentials through the normal auth flow.\",\n );\n }\n if (parsed.search !== \"\") {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: query strings are not allowed on a base URL. Remove the \"?...\" portion.',\n );\n }\n if (parsed.hash !== \"\") {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: URL fragments are not allowed on a base URL. Remove the \"#...\" portion.',\n );\n }\n if (!ALLOWED_BASE_URL_PROTOCOLS.includes(parsed.protocol)) {\n // Do NOT echo baseURL — at this point we've rejected userinfo,\n // query, and fragment, so the remaining secret-leak risk is low,\n // but echoing is inconsistent with the other validation branches\n // and isn't needed to convey the rule to the operator.\n throw new Error(\n \"Invalid AlterConnectConfig.baseURL: only http:// and https:// are allowed.\",\n );\n }\n /* eslint-enable no-unreachable */\n }\n}\n\n/**\n * Merge user config with defaults\n */\nexport function mergeConfig(config: AlterConnectConfig): FullConfig {\n return {\n debug: config.debug ?? false,\n };\n}\n\n/**\n * Debug logging helper\n */\nexport function debugLog(config: FullConfig, ...args: unknown[]): void {\n if (config.debug) {\n // Deliberate console.log: this IS the debug logger, and it only fires\n // when the integrator explicitly opts in via ``config.debug``.\n // eslint-disable-next-line no-console\n console.log(\"[Alter Connect]\", ...args);\n }\n}\n\n/**\n * Export FullConfig type for use in other modules\n */\nexport type { FullConfig };\n","/**\n * Simple event emitter for SDK events\n */\n\n// Variadic ``never[]`` keeps registration contravariant-safe under\n// strictFunctionTypes: any concretely-typed listener (e.g.\n// ``(name: string) => void``) is assignable, while the emitter itself\n// invokes handlers through an ``unknown[]`` cast in emit().\nexport type EventHandler = (...args: never[]) => void;\n\n/**\n * Event emitter for SDK internal events\n */\nexport class EventEmitter {\n private events: Map<string, Set<EventHandler>>;\n\n constructor() {\n this.events = new Map();\n }\n\n /**\n * Register an event listener\n * @returns Unsubscribe function\n */\n on(event: string, handler: EventHandler): () => void {\n if (!this.events.has(event)) {\n this.events.set(event, new Set());\n }\n\n this.events.get(event)!.add(handler);\n\n // Return unsubscribe function\n return () => this.off(event, handler);\n }\n\n /**\n * Remove an event listener\n */\n off(event: string, handler: EventHandler): void {\n const handlers = this.events.get(event);\n if (handlers) {\n handlers.delete(handler);\n }\n }\n\n /**\n * Emit an event\n */\n emit(event: string, ...args: unknown[]): void {\n const handlers = this.events.get(event);\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n (handler as (...emitted: unknown[]) => void)(...args);\n } catch (error) {\n console.error(\n `[Alter Connect] Error in event handler for '${event}':`,\n error,\n );\n }\n });\n }\n }\n\n /**\n * Remove all listeners for an event, or all listeners if no event specified\n */\n removeAllListeners(event?: string): void {\n if (event) {\n this.events.delete(event);\n } else {\n this.events.clear();\n }\n }\n}\n","/**\n * State management for SDK\n */\n\nimport type { AlterError } from \"../types\";\n\n/**\n * SDK internal state\n */\nexport interface SDKState {\n isOpen: boolean;\n sessionToken: string | null;\n error: AlterError | null;\n}\n\ntype StateListener = (state: SDKState) => void;\n\n/**\n * Simple state manager\n */\nexport class StateManager {\n private state: SDKState;\n private listeners: Set<StateListener>;\n\n constructor() {\n this.state = {\n isOpen: false,\n sessionToken: null,\n error: null,\n };\n this.listeners = new Set();\n }\n\n /**\n * Get current state\n */\n getState(): Readonly<SDKState> {\n return { ...this.state };\n }\n\n /**\n * Get a specific state value\n */\n get<K extends keyof SDKState>(key: K): SDKState[K] {\n return this.state[key];\n }\n\n /**\n * Update state\n */\n setState(updates: Partial<SDKState>): void {\n this.state = { ...this.state, ...updates };\n this.notifyListeners();\n }\n\n /**\n * Subscribe to state changes\n */\n subscribe(listener: StateListener): () => void {\n this.listeners.add(listener);\n\n // Return unsubscribe function\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Clear all listeners\n */\n clearListeners(): void {\n this.listeners.clear();\n }\n\n /**\n * Notify all listeners of state change\n */\n private notifyListeners(): void {\n const currentState = this.getState();\n this.listeners.forEach((listener) => {\n try {\n listener(currentState);\n } catch (error) {\n console.error(\"[Alter Connect] Error in state listener:\", error);\n }\n });\n }\n}\n","/**\n * Mobile device detection and responsive utilities\n */\n\n/**\n * Detect if the current device is a mobile device\n */\nexport function isMobileDevice(): boolean {\n // ``window.opera`` (legacy Opera) and ``navigator.msMaxTouchPoints``\n // (IE/old Edge) are non-standard vendor fields absent from lib.dom —\n // narrow structural casts instead of ``any``.\n const userAgent =\n navigator.userAgent ||\n navigator.vendor ||\n (window as Window & { opera?: string }).opera ||\n \"\";\n\n // Check for mobile patterns\n const mobileRegex =\n /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;\n const isMobileUA = mobileRegex.test(userAgent);\n\n // Check for touch capability\n const hasTouchScreen =\n \"ontouchstart\" in window ||\n navigator.maxTouchPoints > 0 ||\n ((navigator as Navigator & { msMaxTouchPoints?: number })\n .msMaxTouchPoints ?? 0) > 0;\n\n // Check screen size\n const isSmallScreen = window.innerWidth <= 768;\n\n return isMobileUA || (hasTouchScreen && isSmallScreen);\n}\n\n/**\n * Detect if the device is specifically a phone (vs tablet)\n */\nexport function isPhoneDevice(): boolean {\n return isMobileDevice() && window.innerWidth <= 480;\n}\n\n/**\n * Detect if the device is a tablet\n */\nexport function isTabletDevice(): boolean {\n return (\n isMobileDevice() && window.innerWidth > 480 && window.innerWidth <= 1024\n );\n}\n\n/**\n * Get the device type\n */\nexport function getDeviceType(): \"desktop\" | \"tablet\" | \"phone\" {\n if (isPhoneDevice()) return \"phone\";\n if (isTabletDevice()) return \"tablet\";\n return \"desktop\";\n}\n\n/**\n * Check if we should use redirect flow instead of popup\n *\n * Popup windows don't work well on mobile:\n * - Browsers often block them\n * - They open as full-screen tabs\n * - Users lose context of parent app\n */\nexport function shouldUseRedirectFlow(): boolean {\n const deviceType = getDeviceType();\n\n // Always use redirect for phones\n if (deviceType === \"phone\") return true;\n\n // Use redirect for tablets in portrait mode (popups are cramped)\n if (deviceType === \"tablet\" && window.innerHeight > window.innerWidth) {\n return true;\n }\n\n // Use popup for desktop and tablets in landscape\n return false;\n}\n","/**\n * OAuth popup and redirect handler\n */\n\nimport type { Grant, AlterError } from \"../types\";\nimport { DEFAULT_ALTER_HOST } from \"../core/config\";\nimport { shouldUseRedirectFlow } from \"../utils/mobile\";\n\ninterface OAuthHandlerOptions {\n baseURL: string;\n onSuccess: (grants: Grant[]) => void;\n onError: (error: AlterError) => void;\n onCancel: () => void;\n popupWidth?: number;\n popupHeight?: number;\n debug?: boolean;\n /** Expected origin for postMessage validation. Derived from the OAuth URL. */\n expectedOrigin?: string;\n}\n\n/**\n * Parse an operation string into a valid Grant['operation'] value\n */\nfunction parseOperation(value: string | null | undefined): Grant[\"operation\"] {\n if (value === \"reauth\") return \"reauth\";\n return \"creation\";\n}\n\n/**\n * Parse a status string into a valid Grant['status'] value\n */\nfunction parseStatus(value: string | null | undefined): Grant[\"status\"] {\n if (value === \"pending\") return \"pending\";\n if (value === \"error\") return \"error\";\n return \"active\";\n}\n\n/**\n * Clean up OAuth redirect state and restore the original URL\n */\nfunction cleanupRedirectState(returnUrl: string): void {\n sessionStorage.removeItem(\"alter_oauth_state\");\n try {\n const cleanUrl = new URL(returnUrl);\n window.history.replaceState(\n {},\n document.title,\n cleanUrl.pathname + cleanUrl.search + cleanUrl.hash,\n );\n } catch {\n // If returnUrl is malformed, just remove query params from current URL\n window.history.replaceState({}, document.title, window.location.pathname);\n }\n}\n\n/**\n * Handles OAuth flow in popup window\n */\nexport class OAuthHandler {\n private popup: Window | null = null;\n private pollInterval: number | null = null;\n private messageListener: ((event: MessageEvent) => void) | null = null;\n private options: Required<OAuthHandlerOptions>;\n private expectedOrigin: string;\n private settled: boolean = false;\n\n constructor(options: OAuthHandlerOptions) {\n this.options = {\n baseURL: options.baseURL,\n onSuccess: options.onSuccess,\n onError: options.onError,\n onCancel: options.onCancel,\n popupWidth: options.popupWidth || 500,\n popupHeight: options.popupHeight || 700,\n debug: options.debug || false,\n expectedOrigin: options.expectedOrigin || \"\",\n };\n\n // Extract origin from baseURL for postMessage validation\n try {\n this.expectedOrigin = new URL(options.baseURL).origin;\n } catch {\n throw new Error(\n `Invalid baseURL: \"${options.baseURL}\". Must be a full URL with protocol (e.g., \"${DEFAULT_ALTER_HOST}\").`,\n );\n }\n }\n\n /**\n * Start OAuth flow (automatically chooses popup or redirect based on device)\n */\n startOAuth(url: string): void {\n // Derive expected origin from the OAuth URL if not explicitly set.\n // SECURITY: Fail-closed — if we can't determine origin, abort the flow.\n if (!this.options.expectedOrigin) {\n try {\n const parsed = new URL(url);\n this.options.expectedOrigin = parsed.origin;\n this.log(\"Derived expected origin:\", this.options.expectedOrigin);\n } catch {\n this.log(\"Failed to parse OAuth URL for origin validation:\", url);\n this.options.onError({\n code: \"invalid_oauth_url\",\n message:\n \"Failed to determine origin from OAuth URL. Cannot proceed securely.\",\n });\n return;\n }\n }\n\n if (shouldUseRedirectFlow()) {\n this.log(\"Using redirect flow for mobile device\");\n this.startRedirectFlow(url);\n } else {\n this.log(\"Using popup flow for desktop\");\n this.openPopup(url);\n }\n }\n\n /**\n * Start OAuth redirect flow (for mobile)\n * Saves state and redirects the entire page\n */\n private startRedirectFlow(url: string): void {\n this.log(\"Starting redirect flow:\", url);\n\n // Save state to sessionStorage to restore after redirect\n const state = {\n timestamp: Date.now(),\n returnUrl: window.location.href,\n };\n\n try {\n sessionStorage.setItem(\"alter_oauth_state\", JSON.stringify(state));\n } catch (error) {\n this.log(\"Failed to save state:\", error);\n this.options.onError({\n code: \"redirect_error\",\n message: \"Failed to start OAuth flow: could not save session state\",\n details: { error },\n });\n return;\n }\n\n // Redirect to OAuth URL (full page redirect)\n window.location.href = url;\n }\n\n /**\n * Check if returning from OAuth redirect and handle result\n * Call this on page load to detect OAuth return\n *\n * @returns true if OAuth return was detected and handled\n */\n static checkOAuthReturn(\n onSuccess: (grants: Grant[]) => void,\n onError: (error: AlterError) => void,\n ): boolean {\n // Check if we have OAuth state saved\n const stateJson = sessionStorage.getItem(\"alter_oauth_state\");\n if (!stateJson) {\n return false;\n }\n\n try {\n const state = JSON.parse(stateJson);\n\n // Check if this is a valid OAuth return (within 5 minutes)\n const elapsed = Date.now() - state.timestamp;\n if (elapsed > 5 * 60 * 1000) {\n // Stale state, clean up\n sessionStorage.removeItem(\"alter_oauth_state\");\n return false;\n }\n\n // Check URL for OAuth callback parameters\n const urlParams = new URLSearchParams(window.location.search);\n const success = urlParams.get(\"alter_connect_success\");\n const error = urlParams.get(\"alter_connect_error\");\n\n if (success === \"true\") {\n // Validate required fields\n const grantId = urlParams.get(\"grant_id\");\n const provider = urlParams.get(\"provider\");\n const accountIdentifier = urlParams.get(\"account_identifier\");\n\n if (!grantId || !provider || !accountIdentifier) {\n cleanupRedirectState(state.returnUrl);\n onError({\n code: \"invalid_response\",\n message: \"OAuth redirect returned incomplete grant data\",\n });\n return true;\n }\n\n const grant: Grant = {\n grant_id: grantId,\n provider: provider,\n provider_name: urlParams.get(\"provider_name\") || provider,\n account_identifier: accountIdentifier,\n timestamp: urlParams.get(\"timestamp\") || new Date().toISOString(),\n operation: parseOperation(urlParams.get(\"operation\")),\n scopes: urlParams.get(\"scopes\")?.split(\",\") || [],\n status: parseStatus(urlParams.get(\"status\")),\n };\n\n cleanupRedirectState(state.returnUrl);\n onSuccess([grant]); // Wrap in array for unified interface\n return true;\n }\n\n if (error) {\n const alterError: AlterError = {\n code: urlParams.get(\"error_code\") || \"oauth_error\",\n message:\n urlParams.get(\"error_description\") || \"OAuth authorization failed\",\n };\n\n cleanupRedirectState(state.returnUrl);\n onError(alterError);\n return true;\n }\n\n return false;\n } catch (err) {\n console.error(\"[OAuth Handler] Failed to check OAuth return:\", err);\n sessionStorage.removeItem(\"alter_oauth_state\");\n return false;\n }\n }\n\n /**\n * Open OAuth popup (for desktop)\n */\n openPopup(url: string): void {\n // Calculate popup position (centered)\n const left =\n window.screenX + (window.outerWidth - this.options.popupWidth) / 2;\n const top =\n window.screenY + (window.outerHeight - this.options.popupHeight) / 2;\n\n const features = [\n `width=${this.options.popupWidth}`,\n `height=${this.options.popupHeight}`,\n `left=${left}`,\n `top=${top}`,\n \"resizable=yes\",\n \"scrollbars=yes\",\n \"status=yes\",\n ].join(\",\");\n\n this.log(\"Opening OAuth popup:\", url);\n\n // Open popup\n this.popup = window.open(url, \"alter_oauth_popup\", features);\n\n if (!this.popup) {\n this.options.onError({\n code: \"popup_blocked\",\n message:\n \"Popup was blocked by browser. Please allow popups for this site.\",\n });\n return;\n }\n\n // Start monitoring popup\n this.startPolling();\n this.setupMessageListener();\n }\n\n /**\n * Close popup and clean up\n */\n close(): void {\n this.log(\"Closing OAuth handler\");\n\n // Close popup\n if (this.popup && !this.popup.closed) {\n this.popup.close();\n }\n this.popup = null;\n\n // Stop polling\n if (this.pollInterval !== null) {\n clearInterval(this.pollInterval);\n this.pollInterval = null;\n }\n\n // Remove message listener\n if (this.messageListener) {\n window.removeEventListener(\"message\", this.messageListener);\n this.messageListener = null;\n }\n }\n\n /**\n * Poll to detect when popup is closed\n */\n private startPolling(): void {\n this.pollInterval = window.setInterval(() => {\n if (this.settled) return;\n if (!this.popup || this.popup.closed) {\n this.log(\"Popup closed by user\");\n this.settled = true;\n this.close();\n this.options.onCancel();\n }\n }, 500);\n }\n\n /**\n * Listen for postMessage from OAuth callback page\n */\n private setupMessageListener(): void {\n this.messageListener = (event: MessageEvent) => {\n // Ignore if already settled (prevents race with polling)\n if (this.settled) return;\n\n // Validate origin against the expected backend origin\n if (event.origin !== this.expectedOrigin) {\n this.log(\n \"Rejected message from unexpected origin:\",\n event.origin,\n \"(expected:\",\n this.expectedOrigin + \")\",\n );\n return;\n }\n\n this.log(\"Received message from\", event.origin);\n\n const data = event.data;\n\n // Check message type\n if (data && typeof data === \"object\") {\n if (data.type === \"alter_connect_success\") {\n this.log(\"OAuth success\");\n\n // Accept both grants array and legacy connections array\n const rawGrants = Array.isArray(data.grants) ? data.grants : null;\n if (rawGrants) {\n this.log(\"Multi-provider success:\", rawGrants.length, \"grants\");\n const grants: Grant[] = [];\n for (const item of rawGrants) {\n const grantId = item.grant_id as string | undefined;\n if (!grantId || !item.provider) {\n this.log(\"Skipping invalid grant item:\", item);\n continue;\n }\n grants.push({\n grant_id: grantId,\n provider: item.provider as string,\n provider_name:\n (item.provider_name as string) || (item.provider as string),\n account_identifier: (item.account_identifier as string) || \"\",\n timestamp:\n (item.timestamp as string) || new Date().toISOString(),\n operation: parseOperation(item.operation as string | undefined),\n scopes: Array.isArray(item.scopes)\n ? (item.scopes as string[])\n : [],\n status: parseStatus(item.status as string | undefined),\n metadata: item.metadata as Grant[\"metadata\"],\n });\n }\n\n if (grants.length === 0) {\n this.settled = true;\n this.close();\n this.options.onError({\n code: \"invalid_response\",\n message: \"Server returned empty grants array\",\n });\n return;\n }\n\n this.settled = true;\n this.close();\n this.options.onSuccess(grants);\n return;\n }\n\n // No valid grants array found\n this.settled = true;\n this.close();\n this.options.onError({\n code: \"invalid_response\",\n message: \"Server returned success without grants array\",\n });\n } else if (data.type === \"alter_connect_error\") {\n this.log(\"OAuth error\");\n\n const error: AlterError = {\n code: (data.error as string) || \"oauth_error\",\n message:\n (data.error_description as string) ||\n \"OAuth authorization failed\",\n details: data as Record<string, unknown>,\n };\n\n this.settled = true;\n this.close();\n this.options.onError(error);\n }\n }\n };\n\n window.addEventListener(\"message\", this.messageListener);\n }\n\n /**\n * Log debug message\n */\n private log(...args: unknown[]): void {\n if (this.options.debug) {\n // Deliberate console.log: this IS the debug logger, and it only fires\n // when the integrator explicitly opts in via ``debug``.\n // eslint-disable-next-line no-console\n console.log(\"[OAuth Handler]\", ...args);\n }\n }\n}\n","/**\n * Main AlterConnect SDK class\n *\n * Opens the backend-served Connect UI in a popup. The backend handles\n * provider selection, branding, and OAuth — then sends the result back via postMessage.\n */\n\nimport type {\n AlterConnectConfig,\n OpenOptions,\n Grant,\n AlterError,\n} from \"../types\";\nimport {\n validateConfig,\n mergeConfig,\n debugLog,\n DEFAULT_ALTER_HOST,\n type FullConfig,\n} from \"./config\";\nimport { EventEmitter, type EventHandler } from \"./events\";\nimport { StateManager } from \"../state/manager\";\nimport { OAuthHandler } from \"../oauth/handler\";\n\n/**\n * SDK version\n */\nconst VERSION = \"0.2.0\";\n\n/**\n * Module-scoped flag so the non-default-host warning fires at most once\n * per process. Reset is intentionally not exposed — the warning exists to\n * surface accidental misconfigurations during dev, not to spam logs.\n */\nlet hasWarnedNonDefaultHost = false;\n\n/**\n * Main SDK class\n */\nexport class AlterConnect {\n private config: FullConfig;\n private eventEmitter: EventEmitter;\n private stateManager: StateManager;\n private _isInitialized: boolean;\n private _oauthHandler: OAuthHandler | null = null;\n private readonly _baseURL: string;\n private _perOpenCleanups: Array<() => void> = [];\n\n /**\n * Private constructor (use AlterConnect.create() instead)\n */\n private constructor(config: AlterConnectConfig = {}) {\n // Validate and merge config\n validateConfig(config);\n this.config = mergeConfig(config);\n\n // Resolve the Alter host once at construction time. `baseURL` is an\n // environment-level setting (production vs self-hosted vs local-dev),\n // not a per-`open()` decision — that's why it lives on the constructor\n // config and not on OpenOptions.\n this._baseURL = (config.baseURL ?? DEFAULT_ALTER_HOST).replace(/\\/+$/, \"\");\n if (this._baseURL !== DEFAULT_ALTER_HOST && !hasWarnedNonDefaultHost) {\n hasWarnedNonDefaultHost = true;\n // Log only the origin, not the full URL. validateConfig rejects\n // userinfo/search/hash, so the path is the only \"extra\" piece the\n // origin strips — but it costs us nothing to be defensive here in\n // case validateConfig's allowlist is ever broadened. The warn\n // message is forwarded to customer-side error trackers (Sentry,\n // Datadog RUM), so minimizing what flows through it is prudent.\n const origin = new URL(this._baseURL).origin;\n console.warn(\n `[alter-connect] Using non-default Alter host: ${origin}. ` +\n \"Unset baseURL in AlterConnect.create() for production.\",\n );\n }\n\n // Initialize components\n this.eventEmitter = new EventEmitter();\n this.stateManager = new StateManager();\n this._isInitialized = true;\n\n debugLog(this.config, \"Alter Connect SDK initialized\", {\n version: VERSION,\n baseURL: this._baseURL,\n });\n\n // Check if returning from OAuth redirect (mobile flow)\n this.checkRedirectReturn();\n }\n\n /**\n * Check if returning from OAuth redirect and handle result\n * This runs automatically on SDK initialization\n */\n private checkRedirectReturn(): void {\n const handled = OAuthHandler.checkOAuthReturn(\n (grants: Grant[]) => {\n debugLog(this.config, \"OAuth redirect return - success:\", grants);\n // Emit success event (will be picked up by event handlers if SDK was re-initialized)\n this.eventEmitter.emit(\"success\", grants);\n },\n (error: AlterError) => {\n debugLog(this.config, \"OAuth redirect return - error:\", error);\n // Emit error event\n this.eventEmitter.emit(\"error\", error);\n },\n );\n\n if (handled) {\n debugLog(this.config, \"OAuth redirect return detected and handled\");\n }\n }\n\n /**\n * Create a new AlterConnect instance\n *\n * @param config Optional configuration\n * @returns AlterConnect instance\n *\n * @example\n * const alterConnect = AlterConnect.create();\n * const alterConnect = AlterConnect.create({ debug: true });\n */\n static create(config?: AlterConnectConfig): AlterConnect {\n return new AlterConnect(config);\n }\n\n /**\n * Open the Connect UI in a popup window\n *\n * Opens the backend-served Connect UI at {baseURL}/oauth/connect#session={token}.\n * The backend handles provider selection, branding, and OAuth flow.\n * On success, the backend sends a postMessage back to this window.\n *\n * @param options Configuration including the session token from your backend\n *\n * @example\n * // 1. Get session token from YOUR backend\n * const { session_token } = await fetch('/api/alter/create-session').then(r => r.json());\n *\n * // 2. Open Connect UI\n * await alterConnect.open({\n * token: session_token,\n * onSuccess: (grants) => {\n * console.log('Connected!', grants);\n * },\n * onError: (error) => {\n * console.error('Failed:', error);\n * }\n * });\n */\n async open(options: OpenOptions): Promise<void> {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call open() - SDK instance has been destroyed\",\n );\n }\n\n debugLog(this.config, \"Opening Connect UI\");\n\n // Validate required options\n if (!options.token || typeof options.token !== \"string\") {\n throw this.createError(\n \"invalid_options\",\n \"Session token is required. Create one from your backend using POST /sdk/oauth/connect/session\",\n );\n }\n\n if (!options.onSuccess || typeof options.onSuccess !== \"function\") {\n throw this.createError(\n \"invalid_options\",\n \"onSuccess callback is required\",\n );\n }\n\n // Check if already open\n if (this.isOpen()) {\n debugLog(this.config, \"Connect UI is already open\");\n return;\n }\n\n // Create OAuth handler first — this validates baseURL and throws on invalid input.\n // We do this BEFORE setting isOpen to avoid leaving the SDK in a broken state.\n this._oauthHandler = new OAuthHandler({\n baseURL: this._baseURL,\n onSuccess: (grants: Grant[]) => {\n debugLog(this.config, \"OAuth success:\", grants);\n this.handleOAuthSuccess(grants);\n },\n onError: (error: AlterError) => {\n debugLog(this.config, \"OAuth error:\", error);\n this.handleOAuthError(error);\n },\n onCancel: () => {\n debugLog(this.config, \"OAuth cancelled (popup closed)\");\n this.handleOAuthCancel();\n },\n popupWidth: 500,\n popupHeight: 700,\n debug: this.config.debug,\n });\n\n // Update state (after handler creation succeeds)\n this.stateManager.setState({\n isOpen: true,\n error: null,\n sessionToken: options.token,\n });\n\n // Register event handlers\n this.registerEventHandlers(options);\n\n // Build the Connect UI URL — the backend serves the full UI\n const connectURL = `${this._baseURL}/sdk/oauth/connect#session=${encodeURIComponent(options.token)}`;\n\n debugLog(this.config, \"Connect URL:\", connectURL);\n\n // Open the Connect UI (popup or redirect depending on device)\n this._oauthHandler.startOAuth(connectURL);\n\n // Emit analytics event\n if (options.onEvent) {\n options.onEvent(\"connect_opened\", {\n timestamp: new Date().toISOString(),\n });\n }\n\n debugLog(this.config, \"Connect UI opened successfully\");\n }\n\n /**\n * Close the Connect UI\n */\n close(): void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call close() - SDK instance has been destroyed\",\n );\n }\n\n debugLog(this.config, \"Closing Connect UI\");\n\n // Clean up OAuth handler if active\n if (this._oauthHandler) {\n this._oauthHandler.close();\n this._oauthHandler = null;\n }\n\n // Remove only per-open handlers (preserves global handlers from on())\n this._perOpenCleanups.forEach((cleanup) => cleanup());\n this._perOpenCleanups = [];\n\n // Update state\n this.stateManager.setState({ isOpen: false });\n\n this.eventEmitter.emit(\"close\");\n }\n\n /**\n * Destroy the SDK instance and clean up resources\n */\n destroy(): void {\n if (!this._isInitialized) {\n // Already destroyed, silently return\n return;\n }\n\n debugLog(this.config, \"Destroying SDK instance\");\n\n // Clean up OAuth handler if active\n if (this._oauthHandler) {\n this._oauthHandler.close();\n this._oauthHandler = null;\n }\n\n // Update state\n if (this.stateManager.get(\"isOpen\")) {\n this.stateManager.setState({ isOpen: false });\n }\n\n // Clean up resources\n this.eventEmitter.removeAllListeners();\n this.stateManager.clearListeners();\n this._isInitialized = false;\n }\n\n /**\n * Register an event listener\n */\n on(event: string, handler: EventHandler): () => void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call on() - SDK instance has been destroyed\",\n );\n }\n\n return this.eventEmitter.on(event, handler);\n }\n\n /**\n * Remove an event listener\n */\n off(event: string, handler: EventHandler): void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call off() - SDK instance has been destroyed\",\n );\n }\n\n this.eventEmitter.off(event, handler);\n }\n\n /**\n * Check if Connect UI is open\n */\n isOpen(): boolean {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call isOpen() - SDK instance has been destroyed\",\n );\n }\n\n return this.stateManager.get(\"isOpen\");\n }\n\n /**\n * Get SDK version\n */\n getVersion(): string {\n return VERSION;\n }\n\n /**\n * Clean up OAuth handler reference after a flow completes.\n * The OAuthHandler.close() is already called internally by the handler\n * before invoking callbacks, so we only need to null the reference here.\n */\n private cleanupHandler(): void {\n this._oauthHandler = null;\n }\n\n /**\n * Handle OAuth success\n */\n private handleOAuthSuccess(grants: Grant[]): void {\n this.cleanupHandler();\n this.eventEmitter.emit(\"success\", grants);\n }\n\n /**\n * Handle OAuth error\n */\n private handleOAuthError(error: AlterError): void {\n this.cleanupHandler();\n this.stateManager.setState({ error });\n this.eventEmitter.emit(\"error\", error);\n }\n\n /**\n * Handle OAuth cancellation (user closed popup)\n */\n private handleOAuthCancel(): void {\n this.cleanupHandler();\n this.eventEmitter.emit(\"exit\");\n }\n\n /**\n * Register event handlers from OpenOptions\n */\n private registerEventHandlers(options: OpenOptions): void {\n // Success handler\n if (options.onSuccess) {\n this._perOpenCleanups.push(\n this.eventEmitter.on(\"success\", (grants: Grant[]) => {\n options.onSuccess(grants);\n this.close();\n }),\n );\n }\n\n // Exit handler\n if (options.onExit) {\n this._perOpenCleanups.push(\n this.eventEmitter.on(\"exit\", () => {\n options.onExit!();\n this.close();\n }),\n );\n }\n\n // Error handler — always registered to ensure close() is called\n this._perOpenCleanups.push(\n this.eventEmitter.on(\"error\", (error: AlterError) => {\n if (options.onError) {\n options.onError(error);\n }\n this.close();\n }),\n );\n\n // Event handler (analytics)\n if (options.onEvent) {\n this._perOpenCleanups.push(\n this.eventEmitter.on(\n \"event\",\n (eventName: string, metadata: Record<string, unknown>) => {\n options.onEvent!(eventName, metadata);\n },\n ),\n );\n }\n }\n\n /**\n * Create an AlterError object\n */\n private createError(\n code: string,\n message: string,\n details?: Record<string, unknown>,\n ): Error {\n const error = new Error(message) as Error & {\n code?: string;\n details?: Record<string, unknown>;\n };\n error.code = code;\n error.details = details;\n return error;\n }\n}\n"],"names":["DEFAULT_ALTER_HOST","debugLog","config","args","debug","console","log","EventEmitter","constructor","this","events","Map","on","event","handler","has","set","Set","get","add","off","handlers","delete","emit","forEach","error","removeAllListeners","clear","StateManager","state","isOpen","sessionToken","listeners","getState","key","setState","updates","notifyListeners","subscribe","listener","clearListeners","currentState","isMobileDevice","userAgent","navigator","vendor","window","opera","isMobileUA","test","hasTouchScreen","maxTouchPoints","msMaxTouchPoints","isSmallScreen","innerWidth","getDeviceType","parseOperation","value","parseStatus","cleanupRedirectState","returnUrl","sessionStorage","removeItem","cleanUrl","URL","history","replaceState","document","title","pathname","search","hash","location","OAuthHandler","options","popup","pollInterval","messageListener","settled","baseURL","onSuccess","onError","onCancel","popupWidth","popupHeight","expectedOrigin","origin","Error","startOAuth","url","parsed","code","message","deviceType","innerHeight","shouldUseRedirectFlow","openPopup","startRedirectFlow","timestamp","Date","now","href","setItem","JSON","stringify","details","checkOAuthReturn","stateJson","getItem","parse","urlParams","URLSearchParams","success","grantId","provider","accountIdentifier","grant","grant_id","provider_name","account_identifier","toISOString","operation","scopes","split","status","alterError","err","left","screenX","outerWidth","top","screenY","outerHeight","features","join","open","startPolling","setupMessageListener","close","closed","clearInterval","removeEventListener","setInterval","data","type","rawGrants","Array","isArray","grants","length","item","push","metadata","error_description","addEventListener","VERSION","hasWarnedNonDefaultHost","AlterConnect","_oauthHandler","_perOpenCleanups","undefined","validateConfig","mergeConfig","_baseURL","replace","warn","eventEmitter","stateManager","_isInitialized","version","checkRedirectReturn","create","createError","token","handleOAuthSuccess","handleOAuthError","handleOAuthCancel","registerEventHandlers","connectURL","encodeURIComponent","onEvent","cleanup","destroy","getVersion","cleanupHandler","onExit","eventName"],"mappings":"oEAcO,MAAMA,EAAqB,yCAwHlBC,EAASC,KAAuBC,GAC1CD,EAAOE,OAITC,QAAQC,IAAI,qBAAsBH,EAEtC,OChIaI,EAGX,WAAAC,GACEC,KAAKC,OAAS,IAAIC,GACpB,CAMA,EAAAC,CAAGC,EAAeC,GAQhB,OAPKL,KAAKC,OAAOK,IAAIF,IACnBJ,KAAKC,OAAOM,IAAIH,EAAO,IAAII,KAG7BR,KAAKC,OAAOQ,IAAIL,GAAQM,IAAIL,GAGrB,IAAML,KAAKW,IAAIP,EAAOC,EAC/B,CAKA,GAAAM,CAAIP,EAAeC,GACjB,MAAMO,EAAWZ,KAAKC,OAAOQ,IAAIL,GAC7BQ,GACFA,EAASC,OAAOR,EAEpB,CAKA,IAAAS,CAAKV,KAAkBV,GACrB,MAAMkB,EAAWZ,KAAKC,OAAOQ,IAAIL,GAC7BQ,GACFA,EAASG,QAASV,IAChB,IACGA,KAA+CX,EAClD,CAAE,MAAOsB,GACPpB,QAAQoB,MACN,+CAA+CZ,MAC/CY,EAEJ,GAGN,CAKA,kBAAAC,CAAmBb,GACbA,EACFJ,KAAKC,OAAOY,OAAOT,GAEnBJ,KAAKC,OAAOiB,OAEhB,QCrDWC,EAIX,WAAApB,GACEC,KAAKoB,MAAQ,CACXC,QAAQ,EACRC,aAAc,KACdN,MAAO,MAEThB,KAAKuB,UAAY,IAAIf,GACvB,CAKA,QAAAgB,GACE,MAAO,IAAKxB,KAAKoB,MACnB,CAKA,GAAAX,CAA8BgB,GAC5B,OAAOzB,KAAKoB,MAAMK,EACpB,CAKA,QAAAC,CAASC,GACP3B,KAAKoB,MAAQ,IAAKpB,KAAKoB,SAAUO,GACjC3B,KAAK4B,iBACP,CAKA,SAAAC,CAAUC,GAIR,OAHA9B,KAAKuB,UAAUb,IAAIoB,GAGZ,KACL9B,KAAKuB,UAAUV,OAAOiB,GAE1B,CAKA,cAAAC,GACE/B,KAAKuB,UAAUL,OACjB,CAKQ,eAAAU,GACN,MAAMI,EAAehC,KAAKwB,WAC1BxB,KAAKuB,UAAUR,QAASe,IACtB,IACEA,EAASE,EACX,CAAE,MAAOhB,GACPpB,QAAQoB,MAAM,2CAA4CA,EAC5D,GAEJ,WC/EciB,IAId,MAAMC,EACJC,UAAUD,WACVC,UAAUC,QACTC,OAAuCC,OACxC,GAKIC,EADJ,iEAC6BC,KAAKN,GAG9BO,EACJ,iBAAkBJ,QAClBF,UAAUO,eAAiB,IACzBP,UACCQ,kBAAoB,GAAK,EAGxBC,EAAgBP,OAAOQ,YAAc,IAE3C,OAAON,GAAeE,GAAkBG,CAC1C,UAqBgBE,IACd,OAhBOb,KAAoBI,OAAOQ,YAAc,IAgBpB,QAR1BZ,KAAoBI,OAAOQ,WAAa,KAAOR,OAAOQ,YAAc,KASzC,SACtB,SACT,CCnCA,SAASE,EAAeC,GACtB,MAAc,WAAVA,EAA2B,SACxB,UACT,CAKA,SAASC,EAAYD,GACnB,MAAc,YAAVA,EAA4B,UAClB,UAAVA,EAA0B,QACvB,QACT,CAKA,SAASE,EAAqBC,GAC5BC,eAAeC,WAAW,qBAC1B,IACE,MAAMC,EAAW,IAAIC,IAAIJ,GACzBd,OAAOmB,QAAQC,aACb,CAAA,EACAC,SAASC,MACTL,EAASM,SAAWN,EAASO,OAASP,EAASQ,KAEnD,CAAE,MAEAzB,OAAOmB,QAAQC,aAAa,CAAA,EAAIC,SAASC,MAAOtB,OAAO0B,SAASH,SAClE,CACF,OAKaI,EAQX,WAAAjE,CAAYkE,GAPJjE,KAAAkE,MAAuB,KACvBlE,KAAAmE,aAA8B,KAC9BnE,KAAAoE,gBAA0D,KAG1DpE,KAAAqE,SAAmB,EAGzBrE,KAAKiE,QAAU,CACbK,QAASL,EAAQK,QACjBC,UAAWN,EAAQM,UACnBC,QAASP,EAAQO,QACjBC,SAAUR,EAAQQ,SAClBC,WAAYT,EAAQS,YAAc,IAClCC,YAAaV,EAAQU,aAAe,IACpChF,MAAOsE,EAAQtE,QAAS,EACxBiF,eAAgBX,EAAQW,gBAAkB,IAI5C,IACE5E,KAAK4E,eAAiB,IAAIrB,IAAIU,EAAQK,SAASO,MACjD,CAAE,MACA,MAAM,IAAIC,MACR,qBAAqBb,EAAQK,sDAAsD/E,OAEvF,CACF,CAKA,UAAAwF,CAAWC,GAGT,IAAKhF,KAAKiE,QAAQW,eAChB,IACE,MAAMK,EAAS,IAAI1B,IAAIyB,GACvBhF,KAAKiE,QAAQW,eAAiBK,EAAOJ,OACrC7E,KAAKH,IAAI,2BAA4BG,KAAKiE,QAAQW,eACpD,CAAE,MAOA,OANA5E,KAAKH,IAAI,mDAAoDmF,QAC7DhF,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,oBACNC,QACE,uEAGN,aDtCJ,MAAMC,EAAatC,IAGnB,MAAmB,UAAfsC,GAGe,WAAfA,GAA2B/C,OAAOgD,YAAchD,OAAOQ,UAM7D,CC6BQyC,IAIFtF,KAAKH,IAAI,gCACTG,KAAKuF,UAAUP,KAJfhF,KAAKH,IAAI,yCACTG,KAAKwF,kBAAkBR,GAK3B,CAMQ,iBAAAQ,CAAkBR,GACxBhF,KAAKH,IAAI,0BAA2BmF,GAGpC,MAAM5D,EAAQ,CACZqE,UAAWC,KAAKC,MAChBxC,UAAWd,OAAO0B,SAAS6B,MAG7B,IACExC,eAAeyC,QAAQ,oBAAqBC,KAAKC,UAAU3E,GAC7D,CAAE,MAAOJ,GAOP,OANAhB,KAAKH,IAAI,wBAAyBmB,QAClChB,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,iBACNC,QAAS,2DACTa,QAAS,CAAEhF,UAGf,CAGAqB,OAAO0B,SAAS6B,KAAOZ,CACzB,CAQA,uBAAOiB,CACL1B,EACAC,GAGA,MAAM0B,EAAY9C,eAAe+C,QAAQ,qBACzC,IAAKD,EACH,OAAO,EAGT,IACE,MAAM9E,EAAQ0E,KAAKM,MAAMF,GAIzB,GADgBR,KAAKC,MAAQvE,EAAMqE,UACrB,IAGZ,OADArC,eAAeC,WAAW,sBACnB,EAIT,MAAMgD,EAAY,IAAIC,gBAAgBjE,OAAO0B,SAASF,QAChD0C,EAAUF,EAAU5F,IAAI,yBACxBO,EAAQqF,EAAU5F,IAAI,uBAE5B,GAAgB,SAAZ8F,EAAoB,CAEtB,MAAMC,EAAUH,EAAU5F,IAAI,YACxBgG,EAAWJ,EAAU5F,IAAI,YACzBiG,EAAoBL,EAAU5F,IAAI,sBAExC,IAAK+F,IAAYC,IAAaC,EAM5B,OALAxD,EAAqB9B,EAAM+B,WAC3BqB,EAAQ,CACNU,KAAM,mBACNC,QAAS,mDAEJ,EAGT,MAAMwB,EAAe,CACnBC,SAAUJ,EACVC,SAAUA,EACVI,cAAeR,EAAU5F,IAAI,kBAAoBgG,EACjDK,mBAAoBJ,EACpBjB,UAAWY,EAAU5F,IAAI,eAAgB,IAAIiF,MAAOqB,cACpDC,UAAWjE,EAAesD,EAAU5F,IAAI,cACxCwG,OAAQZ,EAAU5F,IAAI,WAAWyG,MAAM,MAAQ,GAC/CC,OAAQlE,EAAYoD,EAAU5F,IAAI,YAKpC,OAFAyC,EAAqB9B,EAAM+B,WAC3BoB,EAAU,CAACoC,KACJ,CACT,CAEA,GAAI3F,EAAO,CACT,MAAMoG,EAAyB,CAC7BlC,KAAMmB,EAAU5F,IAAI,eAAiB,cACrC0E,QACEkB,EAAU5F,IAAI,sBAAwB,8BAK1C,OAFAyC,EAAqB9B,EAAM+B,WAC3BqB,EAAQ4C,IACD,CACT,CAEA,OAAO,CACT,CAAE,MAAOC,GAGP,OAFAzH,QAAQoB,MAAM,gDAAiDqG,GAC/DjE,eAAeC,WAAW,sBACnB,CACT,CACF,CAKA,SAAAkC,CAAUP,GAER,MAAMsC,EACJjF,OAAOkF,SAAWlF,OAAOmF,WAAaxH,KAAKiE,QAAQS,YAAc,EAC7D+C,EACJpF,OAAOqF,SAAWrF,OAAOsF,YAAc3H,KAAKiE,QAAQU,aAAe,EAE/DiD,EAAW,CACf,SAAS5H,KAAKiE,QAAQS,aACtB,UAAU1E,KAAKiE,QAAQU,cACvB,QAAQ2C,IACR,OAAOG,IACP,gBACA,iBACA,cACAI,KAAK,KAEP7H,KAAKH,IAAI,uBAAwBmF,GAGjChF,KAAKkE,MAAQ7B,OAAOyF,KAAK9C,EAAK,oBAAqB4C,GAE9C5H,KAAKkE,OAUVlE,KAAK+H,eACL/H,KAAKgI,wBAVHhI,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,gBACNC,QACE,oEAQR,CAKA,KAAA8C,GACEjI,KAAKH,IAAI,yBAGLG,KAAKkE,QAAUlE,KAAKkE,MAAMgE,QAC5BlI,KAAKkE,MAAM+D,QAEbjI,KAAKkE,MAAQ,KAGa,OAAtBlE,KAAKmE,eACPgE,cAAcnI,KAAKmE,cACnBnE,KAAKmE,aAAe,MAIlBnE,KAAKoE,kBACP/B,OAAO+F,oBAAoB,UAAWpI,KAAKoE,iBAC3CpE,KAAKoE,gBAAkB,KAE3B,CAKQ,YAAA2D,GACN/H,KAAKmE,aAAe9B,OAAOgG,YAAY,KACjCrI,KAAKqE,SACJrE,KAAKkE,QAASlE,KAAKkE,MAAMgE,SAC5BlI,KAAKH,IAAI,wBACTG,KAAKqE,SAAU,EACfrE,KAAKiI,QACLjI,KAAKiE,QAAQQ,aAEd,IACL,CAKQ,oBAAAuD,GACNhI,KAAKoE,gBAAmBhE,IAEtB,GAAIJ,KAAKqE,QAAS,OAGlB,GAAIjE,EAAMyE,SAAW7E,KAAK4E,eAOxB,YANA5E,KAAKH,IACH,2CACAO,EAAMyE,OACN,aACA7E,KAAK4E,eAAiB,KAK1B5E,KAAKH,IAAI,wBAAyBO,EAAMyE,QAExC,MAAMyD,EAAOlI,EAAMkI,KAGnB,GAAIA,GAAwB,iBAATA,EACjB,GAAkB,0BAAdA,EAAKC,KAAkC,CACzCvI,KAAKH,IAAI,iBAGT,MAAM2I,EAAYC,MAAMC,QAAQJ,EAAKK,QAAUL,EAAKK,OAAS,KAC7D,GAAIH,EAAW,CACbxI,KAAKH,IAAI,0BAA2B2I,EAAUI,OAAQ,UACtD,MAAMD,EAAkB,GACxB,IAAK,MAAME,KAAQL,EAAW,CAC5B,MAAMhC,EAAUqC,EAAKjC,SAChBJ,GAAYqC,EAAKpC,SAItBkC,EAAOG,KAAK,CACVlC,SAAUJ,EACVC,SAAUoC,EAAKpC,SACfI,cACGgC,EAAKhC,eAA6BgC,EAAKpC,SAC1CK,mBAAqB+B,EAAK/B,oBAAiC,GAC3DrB,UACGoD,EAAKpD,YAAwB,IAAIC,MAAOqB,cAC3CC,UAAWjE,EAAe8F,EAAK7B,WAC/BC,OAAQwB,MAAMC,QAAQG,EAAK5B,QACtB4B,EAAK5B,OACN,GACJE,OAAQlE,EAAY4F,EAAK1B,QACzB4B,SAAUF,EAAKE,WAhBf/I,KAAKH,IAAI,+BAAgCgJ,EAkB7C,CAEA,OAAsB,IAAlBF,EAAOC,QACT5I,KAAKqE,SAAU,EACfrE,KAAKiI,aACLjI,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,mBACNC,QAAS,yCAKbnF,KAAKqE,SAAU,EACfrE,KAAKiI,aACLjI,KAAKiE,QAAQM,UAAUoE,GAEzB,CAGA3I,KAAKqE,SAAU,EACfrE,KAAKiI,QACLjI,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,mBACNC,QAAS,gDAEb,MAAO,GAAkB,wBAAdmD,EAAKC,KAAgC,CAC9CvI,KAAKH,IAAI,eAET,MAAMmB,EAAoB,CACxBkE,KAAOoD,EAAKtH,OAAoB,cAChCmE,QACGmD,EAAKU,mBACN,6BACFhD,QAASsC,GAGXtI,KAAKqE,SAAU,EACfrE,KAAKiI,QACLjI,KAAKiE,QAAQO,QAAQxD,EACvB,GAIJqB,OAAO4G,iBAAiB,UAAWjJ,KAAKoE,gBAC1C,CAKQ,GAAAvE,IAAOH,GACTM,KAAKiE,QAAQtE,OAIfC,QAAQC,IAAI,qBAAsBH,EAEtC,ECzYF,MAAMwJ,EAAU,QAOhB,IAAIC,GAA0B,QAKjBC,EAYX,WAAArJ,CAAoBN,EAA6B,IAU/C,GAjBMO,KAAAqJ,cAAqC,KAErCrJ,KAAAsJ,iBAAsC,GLX1C,SAAyB7J,GAC7B,QAAuB8J,IAAnB9J,EAAO6E,QAYT,MARwB7E,EAAO6E,QAQzB,IAAIQ,MACR,iHAuEN,CKnEI0E,CAAe/J,GACfO,KAAKP,OLuEH,SAAsBA,GAC1B,MAAO,CACLE,MAAOF,EAAOE,QAAS,EAE3B,CK3EkB8J,CAAYhK,GAM1BO,KAAK0J,UAAYjK,EAAO6E,SAAW/E,GAAoBoK,QAAQ,OAAQ,IACnE3J,KAAK0J,WAAanK,IAAuB4J,EAAyB,CACpEA,GAA0B,EAO1B,MAAMtE,EAAS,IAAItB,IAAIvD,KAAK0J,UAAU7E,OACtCjF,QAAQgK,KACN,iDAAiD/E,4DAGrD,CAGA7E,KAAK6J,aAAe,IAAI/J,EACxBE,KAAK8J,aAAe,IAAI3I,EACxBnB,KAAK+J,gBAAiB,EAEtBvK,EAASQ,KAAKP,OAAQ,gCAAiC,CACrDuK,QAASd,EACT5E,QAAStE,KAAK0J,WAIhB1J,KAAKiK,qBACP,CAMQ,mBAAAA,GACUjG,EAAaiC,iBAC1B0C,IACCnJ,EAASQ,KAAKP,OAAQ,mCAAoCkJ,GAE1D3I,KAAK6J,aAAa/I,KAAK,UAAW6H,IAEnC3H,IACCxB,EAASQ,KAAKP,OAAQ,iCAAkCuB,GAExDhB,KAAK6J,aAAa/I,KAAK,QAASE,MAKlCxB,EAASQ,KAAKP,OAAQ,6CAE1B,CAYA,aAAOyK,CAAOzK,GACZ,OAAO,IAAI2J,EAAa3J,EAC1B,CA0BA,UAAMqI,CAAK7D,GAET,IAAKjE,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,wDAOJ,GAHA3K,EAASQ,KAAKP,OAAQ,uBAGjBwE,EAAQmG,OAAkC,iBAAlBnG,EAAQmG,MACnC,MAAMpK,KAAKmK,YACT,kBACA,iGAIJ,IAAKlG,EAAQM,WAA0C,mBAAtBN,EAAQM,UACvC,MAAMvE,KAAKmK,YACT,kBACA,kCAKJ,GAAInK,KAAKqB,SAEP,YADA7B,EAASQ,KAAKP,OAAQ,8BAMxBO,KAAKqJ,cAAgB,IAAIrF,EAAa,CACpCM,QAAStE,KAAK0J,SACdnF,UAAYoE,IACVnJ,EAASQ,KAAKP,OAAQ,iBAAkBkJ,GACxC3I,KAAKqK,mBAAmB1B,IAE1BnE,QAAUxD,IACRxB,EAASQ,KAAKP,OAAQ,eAAgBuB,GACtChB,KAAKsK,iBAAiBtJ,IAExByD,SAAU,KACRjF,EAASQ,KAAKP,OAAQ,kCACtBO,KAAKuK,qBAEP7F,WAAY,IACZC,YAAa,IACbhF,MAAOK,KAAKP,OAAOE,QAIrBK,KAAK8J,aAAapI,SAAS,CACzBL,QAAQ,EACRL,MAAO,KACPM,aAAc2C,EAAQmG,QAIxBpK,KAAKwK,sBAAsBvG,GAG3B,MAAMwG,EAAa,GAAGzK,KAAK0J,sCAAsCgB,mBAAmBzG,EAAQmG,SAE5F5K,EAASQ,KAAKP,OAAQ,eAAgBgL,GAGtCzK,KAAKqJ,cAActE,WAAW0F,GAG1BxG,EAAQ0G,SACV1G,EAAQ0G,QAAQ,iBAAkB,CAChClF,WAAW,IAAIC,MAAOqB,gBAI1BvH,EAASQ,KAAKP,OAAQ,iCACxB,CAKA,KAAAwI,GAEE,IAAKjI,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,yDAIJ3K,EAASQ,KAAKP,OAAQ,sBAGlBO,KAAKqJ,gBACPrJ,KAAKqJ,cAAcpB,QACnBjI,KAAKqJ,cAAgB,MAIvBrJ,KAAKsJ,iBAAiBvI,QAAS6J,GAAYA,KAC3C5K,KAAKsJ,iBAAmB,GAGxBtJ,KAAK8J,aAAapI,SAAS,CAAEL,QAAQ,IAErCrB,KAAK6J,aAAa/I,KAAK,QACzB,CAKA,OAAA+J,GACO7K,KAAK+J,iBAKVvK,EAASQ,KAAKP,OAAQ,2BAGlBO,KAAKqJ,gBACPrJ,KAAKqJ,cAAcpB,QACnBjI,KAAKqJ,cAAgB,MAInBrJ,KAAK8J,aAAarJ,IAAI,WACxBT,KAAK8J,aAAapI,SAAS,CAAEL,QAAQ,IAIvCrB,KAAK6J,aAAa5I,qBAClBjB,KAAK8J,aAAa/H,iBAClB/B,KAAK+J,gBAAiB,EACxB,CAKA,EAAA5J,CAAGC,EAAeC,GAEhB,IAAKL,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,sDAIJ,OAAOnK,KAAK6J,aAAa1J,GAAGC,EAAOC,EACrC,CAKA,GAAAM,CAAIP,EAAeC,GAEjB,IAAKL,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,uDAIJnK,KAAK6J,aAAalJ,IAAIP,EAAOC,EAC/B,CAKA,MAAAgB,GAEE,IAAKrB,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,0DAIJ,OAAOnK,KAAK8J,aAAarJ,IAAI,SAC/B,CAKA,UAAAqK,GACE,OAAO5B,CACT,CAOQ,cAAA6B,GACN/K,KAAKqJ,cAAgB,IACvB,CAKQ,kBAAAgB,CAAmB1B,GACzB3I,KAAK+K,iBACL/K,KAAK6J,aAAa/I,KAAK,UAAW6H,EACpC,CAKQ,gBAAA2B,CAAiBtJ,GACvBhB,KAAK+K,iBACL/K,KAAK8J,aAAapI,SAAS,CAAEV,UAC7BhB,KAAK6J,aAAa/I,KAAK,QAASE,EAClC,CAKQ,iBAAAuJ,GACNvK,KAAK+K,iBACL/K,KAAK6J,aAAa/I,KAAK,OACzB,CAKQ,qBAAA0J,CAAsBvG,GAExBA,EAAQM,WACVvE,KAAKsJ,iBAAiBR,KACpB9I,KAAK6J,aAAa1J,GAAG,UAAYwI,IAC/B1E,EAAQM,UAAUoE,GAClB3I,KAAKiI,WAMPhE,EAAQ+G,QACVhL,KAAKsJ,iBAAiBR,KACpB9I,KAAK6J,aAAa1J,GAAG,OAAQ,KAC3B8D,EAAQ+G,SACRhL,KAAKiI,WAMXjI,KAAKsJ,iBAAiBR,KACpB9I,KAAK6J,aAAa1J,GAAG,QAAUa,IACzBiD,EAAQO,SACVP,EAAQO,QAAQxD,GAElBhB,KAAKiI,WAKLhE,EAAQ0G,SACV3K,KAAKsJ,iBAAiBR,KACpB9I,KAAK6J,aAAa1J,GAChB,QACA,CAAC8K,EAAmBlC,KAClB9E,EAAQ0G,QAASM,EAAWlC,KAKtC,CAKQ,WAAAoB,CACNjF,EACAC,EACAa,GAEA,MAAMhF,EAAQ,IAAI8D,MAAMK,GAMxB,OAFAnE,EAAMkE,KAAOA,EACblE,EAAMgF,QAAUA,EACThF,CACT"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e="https://backend.alterauth.com";function t(e,...t){e.debug&&console.log("[Alter Connect]",...t)}class s{constructor(){this.events=new Map}on(e,t){return this.events.has(e)||this.events.set(e,new Set),this.events.get(e).add(t),()=>this.off(e,t)}off(e,t){const s=this.events.get(e);s&&s.delete(t)}emit(e,...t){const s=this.events.get(e);s&&s.forEach(s=>{try{s(...t)}catch(t){console.error(`[Alter Connect] Error in event handler for '${e}':`,t)}})}removeAllListeners(e){e?this.events.delete(e):this.events.clear()}}class i{constructor(){this.state={isOpen:!1,sessionToken:null,error:null},this.listeners=new Set}getState(){return{...this.state}}get(e){return this.state[e]}setState(e){this.state={...this.state,...e},this.notifyListeners()}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}clearListeners(){this.listeners.clear()}notifyListeners(){const e=this.getState();this.listeners.forEach(t=>{try{t(e)}catch(e){console.error("[Alter Connect] Error in state listener:",e)}})}}function n(){const e=navigator.userAgent||navigator.vendor||window.opera,t=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(e),s="ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0,i=window.innerWidth<=768;return t||s&&i}function r(){return n()&&window.innerWidth<=480?"phone":n()&&window.innerWidth>480&&window.innerWidth<=1024?"tablet":"desktop"}function o(e){return"reauth"===e?"reauth":"creation"}function a(e){return"pending"===e?"pending":"error"===e?"error":"active"}function h(e){sessionStorage.removeItem("alter_oauth_state");try{const t=new URL(e);window.history.replaceState({},document.title,t.pathname+t.search+t.hash)}catch{window.history.replaceState({},document.title,window.location.pathname)}}class c{constructor(t){this.popup=null,this.pollInterval=null,this.messageListener=null,this.settled=!1,this.options={baseURL:t.baseURL,onSuccess:t.onSuccess,onError:t.onError,onCancel:t.onCancel,popupWidth:t.popupWidth||500,popupHeight:t.popupHeight||700,debug:t.debug||!1,expectedOrigin:t.expectedOrigin||""};try{this.expectedOrigin=new URL(t.baseURL).origin}catch{throw new Error(`Invalid baseURL: "${t.baseURL}". Must be a full URL with protocol (e.g., "${e}").`)}}startOAuth(e){if(!this.options.expectedOrigin)try{const t=new URL(e);this.options.expectedOrigin=t.origin,this.log("Derived expected origin:",this.options.expectedOrigin)}catch{return this.log("Failed to parse OAuth URL for origin validation:",e),void this.options.onError({code:"invalid_oauth_url",message:"Failed to determine origin from OAuth URL. Cannot proceed securely."})}!function(){const e=r();return"phone"===e||"tablet"===e&&window.innerHeight>window.innerWidth}()?(this.log("Using popup flow for desktop"),this.openPopup(e)):(this.log("Using redirect flow for mobile device"),this.startRedirectFlow(e))}startRedirectFlow(e){this.log("Starting redirect flow:",e);const t={timestamp:Date.now(),returnUrl:window.location.href};try{sessionStorage.setItem("alter_oauth_state",JSON.stringify(t))}catch(e){return this.log("Failed to save state:",e),void this.options.onError({code:"redirect_error",message:"Failed to start OAuth flow: could not save session state",details:{error:e}})}window.location.href=e}static checkOAuthReturn(e,t){const s=sessionStorage.getItem("alter_oauth_state");if(!s)return!1;try{const i=JSON.parse(s);if(Date.now()-i.timestamp>3e5)return sessionStorage.removeItem("alter_oauth_state"),!1;const n=new URLSearchParams(window.location.search),r=n.get("alter_connect_success"),c=n.get("alter_connect_error");if("true"===r){const s=n.get("grant_id"),r=n.get("provider"),c=n.get("account_identifier");if(!s||!r||!c)return h(i.returnUrl),t({code:"invalid_response",message:"OAuth redirect returned incomplete grant data"}),!0;const l={grant_id:s,provider:r,provider_name:n.get("provider_name")||r,account_identifier:c,timestamp:n.get("timestamp")||(new Date).toISOString(),operation:o(n.get("operation")),scopes:n.get("scopes")?.split(",")||[],status:a(n.get("status"))};return h(i.returnUrl),e([l]),!0}if(c){const e={code:n.get("error_code")||"oauth_error",message:n.get("error_description")||"OAuth authorization failed"};return h(i.returnUrl),t(e),!0}return!1}catch(e){return console.error("[OAuth Handler] Failed to check OAuth return:",e),sessionStorage.removeItem("alter_oauth_state"),!1}}openPopup(e){const t=window.screenX+(window.outerWidth-this.options.popupWidth)/2,s=window.screenY+(window.outerHeight-this.options.popupHeight)/2,i=[`width=${this.options.popupWidth}`,`height=${this.options.popupHeight}`,`left=${t}`,`top=${s}`,"resizable=yes","scrollbars=yes","status=yes"].join(",");this.log("Opening OAuth popup:",e),this.popup=window.open(e,"alter_oauth_popup",i),this.popup?(this.startPolling(),this.setupMessageListener()):this.options.onError({code:"popup_blocked",message:"Popup was blocked by browser. Please allow popups for this site."})}close(){this.log("Closing OAuth handler"),this.popup&&!this.popup.closed&&this.popup.close(),this.popup=null,null!==this.pollInterval&&(clearInterval(this.pollInterval),this.pollInterval=null),this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null)}startPolling(){this.pollInterval=window.setInterval(()=>{this.settled||this.popup&&!this.popup.closed||(this.log("Popup closed by user"),this.settled=!0,this.close(),this.options.onCancel())},500)}setupMessageListener(){this.messageListener=e=>{if(this.settled)return;if(e.origin!==this.expectedOrigin)return void this.log("Rejected message from unexpected origin:",e.origin,"(expected:",this.expectedOrigin+")");this.log("Received message from",e.origin);const t=e.data;if(t&&"object"==typeof t)if("alter_connect_success"===t.type){this.log("OAuth success");const e=Array.isArray(t.grants)?t.grants:null;if(e){this.log("Multi-provider success:",e.length,"grants");const t=[];for(const s of e){const e=s.grant_id;e&&s.provider?t.push({grant_id:e,provider:s.provider,provider_name:s.provider_name||s.provider,account_identifier:s.account_identifier||"",timestamp:s.timestamp||(new Date).toISOString(),operation:o(s.operation),scopes:Array.isArray(s.scopes)?s.scopes:[],status:a(s.status),metadata:s.metadata}):this.log("Skipping invalid grant item:",s)}return 0===t.length?(this.settled=!0,this.close(),void this.options.onError({code:"invalid_response",message:"Server returned empty grants array"})):(this.settled=!0,this.close(),void this.options.onSuccess(t))}this.settled=!0,this.close(),this.options.onError({code:"invalid_response",message:"Server returned success without grants array"})}else if("alter_connect_error"===t.type){this.log("OAuth error");const e={code:t.error||"oauth_error",message:t.error_description||"OAuth authorization failed",details:t};this.settled=!0,this.close(),this.options.onError(e)}},window.addEventListener("message",this.messageListener)}log(...e){this.options.debug&&console.log("[OAuth Handler]",...e)}}const l="0.2.0";let d=!1;class u{constructor(n={}){if(this._oauthHandler=null,this._perOpenCleanups=[],function(e){if(void 0!==e.baseURL)throw e.baseURL,new Error("AlterConnectConfig.baseURL is reserved and not yet supported. Omit the field to use the production Alter host.")}(n),this.config=function(e){return{debug:e.debug??!1}}(n),this._baseURL=(n.baseURL??e).replace(/\/+$/,""),this._baseURL!==e&&!d){d=!0;const e=new URL(this._baseURL).origin;console.warn(`[alter-connect] Using non-default Alter host: ${e}. Unset baseURL in AlterConnect.create() for production.`)}this.eventEmitter=new s,this.stateManager=new i,this._isInitialized=!0,t(this.config,"Alter Connect SDK initialized",{version:l,baseURL:this._baseURL}),this.checkRedirectReturn()}checkRedirectReturn(){c.checkOAuthReturn(e=>{t(this.config,"OAuth redirect return - success:",e),this.eventEmitter.emit("success",e)},e=>{t(this.config,"OAuth redirect return - error:",e),this.eventEmitter.emit("error",e)})&&t(this.config,"OAuth redirect return detected and handled")}static create(e){return new u(e)}async open(e){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call open() - SDK instance has been destroyed");if(t(this.config,"Opening Connect UI"),!e.token||"string"!=typeof e.token)throw this.createError("invalid_options","Session token is required. Create one from your backend using POST /sdk/oauth/connect/session");if(!e.onSuccess||"function"!=typeof e.onSuccess)throw this.createError("invalid_options","onSuccess callback is required");if(this.isOpen())return void t(this.config,"Connect UI is already open");this._oauthHandler=new c({baseURL:this._baseURL,onSuccess:e=>{t(this.config,"OAuth success:",e),this.handleOAuthSuccess(e)},onError:e=>{t(this.config,"OAuth error:",e),this.handleOAuthError(e)},onCancel:()=>{t(this.config,"OAuth cancelled (popup closed)"),this.handleOAuthCancel()},popupWidth:500,popupHeight:700,debug:this.config.debug}),this.stateManager.setState({isOpen:!0,error:null,sessionToken:e.token}),this.registerEventHandlers(e);const s=`${this._baseURL}/sdk/oauth/connect#session=${encodeURIComponent(e.token)}`;t(this.config,"Connect URL:",s),this._oauthHandler.startOAuth(s),e.onEvent&&e.onEvent("connect_opened",{timestamp:(new Date).toISOString()}),t(this.config,"Connect UI opened successfully")}close(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call close() - SDK instance has been destroyed");t(this.config,"Closing Connect UI"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this._perOpenCleanups.forEach(e=>e()),this._perOpenCleanups=[],this.stateManager.setState({isOpen:!1}),this.eventEmitter.emit("close")}destroy(){this._isInitialized&&(t(this.config,"Destroying SDK instance"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this.stateManager.get("isOpen")&&this.stateManager.setState({isOpen:!1}),this.eventEmitter.removeAllListeners(),this.stateManager.clearListeners(),this._isInitialized=!1)}on(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call on() - SDK instance has been destroyed");return this.eventEmitter.on(e,t)}off(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call off() - SDK instance has been destroyed");this.eventEmitter.off(e,t)}isOpen(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call isOpen() - SDK instance has been destroyed");return this.stateManager.get("isOpen")}getVersion(){return l}cleanupHandler(){this._oauthHandler=null}handleOAuthSuccess(e){this.cleanupHandler(),this.eventEmitter.emit("success",e)}handleOAuthError(e){this.cleanupHandler(),this.stateManager.setState({error:e}),this.eventEmitter.emit("error",e)}handleOAuthCancel(){this.cleanupHandler(),this.eventEmitter.emit("exit")}registerEventHandlers(e){e.onSuccess&&this._perOpenCleanups.push(this.eventEmitter.on("success",t=>{e.onSuccess(t),this.close()})),e.onExit&&this._perOpenCleanups.push(this.eventEmitter.on("exit",()=>{e.onExit(),this.close()})),this._perOpenCleanups.push(this.eventEmitter.on("error",t=>{e.onError&&e.onError(t),this.close()})),e.onEvent&&this._perOpenCleanups.push(this.eventEmitter.on("event",(t,s)=>{e.onEvent(t,s)}))}createError(e,t,s){const i=new Error(t);return i.code=e,i.details=s,i}}export{u as default};
|
|
1
|
+
const e="https://backend.alterauth.com";function t(e,...t){e.debug&&console.log("[Alter Connect]",...t)}class s{constructor(){this.events=new Map}on(e,t){return this.events.has(e)||this.events.set(e,new Set),this.events.get(e).add(t),()=>this.off(e,t)}off(e,t){const s=this.events.get(e);s&&s.delete(t)}emit(e,...t){const s=this.events.get(e);s&&s.forEach(s=>{try{s(...t)}catch(t){console.error(`[Alter Connect] Error in event handler for '${e}':`,t)}})}removeAllListeners(e){e?this.events.delete(e):this.events.clear()}}class i{constructor(){this.state={isOpen:!1,sessionToken:null,error:null},this.listeners=new Set}getState(){return{...this.state}}get(e){return this.state[e]}setState(e){this.state={...this.state,...e},this.notifyListeners()}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}clearListeners(){this.listeners.clear()}notifyListeners(){const e=this.getState();this.listeners.forEach(t=>{try{t(e)}catch(e){console.error("[Alter Connect] Error in state listener:",e)}})}}function n(){const e=navigator.userAgent||navigator.vendor||window.opera||"",t=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(e),s="ontouchstart"in window||navigator.maxTouchPoints>0||(navigator.msMaxTouchPoints??0)>0,i=window.innerWidth<=768;return t||s&&i}function r(){return n()&&window.innerWidth<=480?"phone":n()&&window.innerWidth>480&&window.innerWidth<=1024?"tablet":"desktop"}function o(e){return"reauth"===e?"reauth":"creation"}function a(e){return"pending"===e?"pending":"error"===e?"error":"active"}function h(e){sessionStorage.removeItem("alter_oauth_state");try{const t=new URL(e);window.history.replaceState({},document.title,t.pathname+t.search+t.hash)}catch{window.history.replaceState({},document.title,window.location.pathname)}}class c{constructor(t){this.popup=null,this.pollInterval=null,this.messageListener=null,this.settled=!1,this.options={baseURL:t.baseURL,onSuccess:t.onSuccess,onError:t.onError,onCancel:t.onCancel,popupWidth:t.popupWidth||500,popupHeight:t.popupHeight||700,debug:t.debug||!1,expectedOrigin:t.expectedOrigin||""};try{this.expectedOrigin=new URL(t.baseURL).origin}catch{throw new Error(`Invalid baseURL: "${t.baseURL}". Must be a full URL with protocol (e.g., "${e}").`)}}startOAuth(e){if(!this.options.expectedOrigin)try{const t=new URL(e);this.options.expectedOrigin=t.origin,this.log("Derived expected origin:",this.options.expectedOrigin)}catch{return this.log("Failed to parse OAuth URL for origin validation:",e),void this.options.onError({code:"invalid_oauth_url",message:"Failed to determine origin from OAuth URL. Cannot proceed securely."})}!function(){const e=r();return"phone"===e||"tablet"===e&&window.innerHeight>window.innerWidth}()?(this.log("Using popup flow for desktop"),this.openPopup(e)):(this.log("Using redirect flow for mobile device"),this.startRedirectFlow(e))}startRedirectFlow(e){this.log("Starting redirect flow:",e);const t={timestamp:Date.now(),returnUrl:window.location.href};try{sessionStorage.setItem("alter_oauth_state",JSON.stringify(t))}catch(e){return this.log("Failed to save state:",e),void this.options.onError({code:"redirect_error",message:"Failed to start OAuth flow: could not save session state",details:{error:e}})}window.location.href=e}static checkOAuthReturn(e,t){const s=sessionStorage.getItem("alter_oauth_state");if(!s)return!1;try{const i=JSON.parse(s);if(Date.now()-i.timestamp>3e5)return sessionStorage.removeItem("alter_oauth_state"),!1;const n=new URLSearchParams(window.location.search),r=n.get("alter_connect_success"),c=n.get("alter_connect_error");if("true"===r){const s=n.get("grant_id"),r=n.get("provider"),c=n.get("account_identifier");if(!s||!r||!c)return h(i.returnUrl),t({code:"invalid_response",message:"OAuth redirect returned incomplete grant data"}),!0;const l={grant_id:s,provider:r,provider_name:n.get("provider_name")||r,account_identifier:c,timestamp:n.get("timestamp")||(new Date).toISOString(),operation:o(n.get("operation")),scopes:n.get("scopes")?.split(",")||[],status:a(n.get("status"))};return h(i.returnUrl),e([l]),!0}if(c){const e={code:n.get("error_code")||"oauth_error",message:n.get("error_description")||"OAuth authorization failed"};return h(i.returnUrl),t(e),!0}return!1}catch(e){return console.error("[OAuth Handler] Failed to check OAuth return:",e),sessionStorage.removeItem("alter_oauth_state"),!1}}openPopup(e){const t=window.screenX+(window.outerWidth-this.options.popupWidth)/2,s=window.screenY+(window.outerHeight-this.options.popupHeight)/2,i=[`width=${this.options.popupWidth}`,`height=${this.options.popupHeight}`,`left=${t}`,`top=${s}`,"resizable=yes","scrollbars=yes","status=yes"].join(",");this.log("Opening OAuth popup:",e),this.popup=window.open(e,"alter_oauth_popup",i),this.popup?(this.startPolling(),this.setupMessageListener()):this.options.onError({code:"popup_blocked",message:"Popup was blocked by browser. Please allow popups for this site."})}close(){this.log("Closing OAuth handler"),this.popup&&!this.popup.closed&&this.popup.close(),this.popup=null,null!==this.pollInterval&&(clearInterval(this.pollInterval),this.pollInterval=null),this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null)}startPolling(){this.pollInterval=window.setInterval(()=>{this.settled||this.popup&&!this.popup.closed||(this.log("Popup closed by user"),this.settled=!0,this.close(),this.options.onCancel())},500)}setupMessageListener(){this.messageListener=e=>{if(this.settled)return;if(e.origin!==this.expectedOrigin)return void this.log("Rejected message from unexpected origin:",e.origin,"(expected:",this.expectedOrigin+")");this.log("Received message from",e.origin);const t=e.data;if(t&&"object"==typeof t)if("alter_connect_success"===t.type){this.log("OAuth success");const e=Array.isArray(t.grants)?t.grants:null;if(e){this.log("Multi-provider success:",e.length,"grants");const t=[];for(const s of e){const e=s.grant_id;e&&s.provider?t.push({grant_id:e,provider:s.provider,provider_name:s.provider_name||s.provider,account_identifier:s.account_identifier||"",timestamp:s.timestamp||(new Date).toISOString(),operation:o(s.operation),scopes:Array.isArray(s.scopes)?s.scopes:[],status:a(s.status),metadata:s.metadata}):this.log("Skipping invalid grant item:",s)}return 0===t.length?(this.settled=!0,this.close(),void this.options.onError({code:"invalid_response",message:"Server returned empty grants array"})):(this.settled=!0,this.close(),void this.options.onSuccess(t))}this.settled=!0,this.close(),this.options.onError({code:"invalid_response",message:"Server returned success without grants array"})}else if("alter_connect_error"===t.type){this.log("OAuth error");const e={code:t.error||"oauth_error",message:t.error_description||"OAuth authorization failed",details:t};this.settled=!0,this.close(),this.options.onError(e)}},window.addEventListener("message",this.messageListener)}log(...e){this.options.debug&&console.log("[OAuth Handler]",...e)}}const l="0.2.0";let d=!1;class u{constructor(n={}){if(this._oauthHandler=null,this._perOpenCleanups=[],function(e){if(void 0!==e.baseURL)throw e.baseURL,new Error("AlterConnectConfig.baseURL is reserved and not yet supported. Omit the field to use the production Alter host.")}(n),this.config=function(e){return{debug:e.debug??!1}}(n),this._baseURL=(n.baseURL??e).replace(/\/+$/,""),this._baseURL!==e&&!d){d=!0;const e=new URL(this._baseURL).origin;console.warn(`[alter-connect] Using non-default Alter host: ${e}. Unset baseURL in AlterConnect.create() for production.`)}this.eventEmitter=new s,this.stateManager=new i,this._isInitialized=!0,t(this.config,"Alter Connect SDK initialized",{version:l,baseURL:this._baseURL}),this.checkRedirectReturn()}checkRedirectReturn(){c.checkOAuthReturn(e=>{t(this.config,"OAuth redirect return - success:",e),this.eventEmitter.emit("success",e)},e=>{t(this.config,"OAuth redirect return - error:",e),this.eventEmitter.emit("error",e)})&&t(this.config,"OAuth redirect return detected and handled")}static create(e){return new u(e)}async open(e){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call open() - SDK instance has been destroyed");if(t(this.config,"Opening Connect UI"),!e.token||"string"!=typeof e.token)throw this.createError("invalid_options","Session token is required. Create one from your backend using POST /sdk/oauth/connect/session");if(!e.onSuccess||"function"!=typeof e.onSuccess)throw this.createError("invalid_options","onSuccess callback is required");if(this.isOpen())return void t(this.config,"Connect UI is already open");this._oauthHandler=new c({baseURL:this._baseURL,onSuccess:e=>{t(this.config,"OAuth success:",e),this.handleOAuthSuccess(e)},onError:e=>{t(this.config,"OAuth error:",e),this.handleOAuthError(e)},onCancel:()=>{t(this.config,"OAuth cancelled (popup closed)"),this.handleOAuthCancel()},popupWidth:500,popupHeight:700,debug:this.config.debug}),this.stateManager.setState({isOpen:!0,error:null,sessionToken:e.token}),this.registerEventHandlers(e);const s=`${this._baseURL}/sdk/oauth/connect#session=${encodeURIComponent(e.token)}`;t(this.config,"Connect URL:",s),this._oauthHandler.startOAuth(s),e.onEvent&&e.onEvent("connect_opened",{timestamp:(new Date).toISOString()}),t(this.config,"Connect UI opened successfully")}close(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call close() - SDK instance has been destroyed");t(this.config,"Closing Connect UI"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this._perOpenCleanups.forEach(e=>e()),this._perOpenCleanups=[],this.stateManager.setState({isOpen:!1}),this.eventEmitter.emit("close")}destroy(){this._isInitialized&&(t(this.config,"Destroying SDK instance"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this.stateManager.get("isOpen")&&this.stateManager.setState({isOpen:!1}),this.eventEmitter.removeAllListeners(),this.stateManager.clearListeners(),this._isInitialized=!1)}on(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call on() - SDK instance has been destroyed");return this.eventEmitter.on(e,t)}off(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call off() - SDK instance has been destroyed");this.eventEmitter.off(e,t)}isOpen(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call isOpen() - SDK instance has been destroyed");return this.stateManager.get("isOpen")}getVersion(){return l}cleanupHandler(){this._oauthHandler=null}handleOAuthSuccess(e){this.cleanupHandler(),this.eventEmitter.emit("success",e)}handleOAuthError(e){this.cleanupHandler(),this.stateManager.setState({error:e}),this.eventEmitter.emit("error",e)}handleOAuthCancel(){this.cleanupHandler(),this.eventEmitter.emit("exit")}registerEventHandlers(e){e.onSuccess&&this._perOpenCleanups.push(this.eventEmitter.on("success",t=>{e.onSuccess(t),this.close()})),e.onExit&&this._perOpenCleanups.push(this.eventEmitter.on("exit",()=>{e.onExit(),this.close()})),this._perOpenCleanups.push(this.eventEmitter.on("error",t=>{e.onError&&e.onError(t),this.close()})),e.onEvent&&this._perOpenCleanups.push(this.eventEmitter.on("event",(t,s)=>{e.onEvent(t,s)}))}createError(e,t,s){const i=new Error(t);return i.code=e,i.details=s,i}}export{u as default};
|
|
2
2
|
//# sourceMappingURL=alter-connect.esm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"alter-connect.esm.js","sources":["../src/core/config.ts","../src/core/events.ts","../src/state/manager.ts","../src/utils/mobile.ts","../src/oauth/handler.ts","../src/core/alter-connect.ts"],"sourcesContent":["/**\n * Configuration validation and management\n */\n\nimport type { AlterConnectConfig } from '../types';\n\n/**\n * Canonical Alter Connect host.\n *\n * Single source of truth for the widget's default host. `alter-connect.ts`\n * uses it as the constructor fallback; `config.ts` and `oauth/handler.ts`\n * surface it in error messages so operators always see the same example\n * URL no matter which validation layer flags their misconfig.\n */\nexport const DEFAULT_ALTER_HOST = 'https://backend.alterauth.com';\n\n/**\n * Internal config used by the SDK\n */\ninterface FullConfig {\n debug: boolean;\n}\n\n/**\n * Validate configuration.\n *\n * `baseURL` is RESERVED in the public type for future self-hosted /\n * non-production support. It is not yet accepted at runtime — see the\n * `REMOVE_TO_ENABLE_BASE_URL_OVERRIDE` block below. The deep validation\n * beneath it is intentionally kept so re-enabling is a single-line\n * deletion; each check retains its inline rationale so future-readers\n * don't have to reconstruct the reasoning.\n */\nconst ALLOWED_BASE_URL_PROTOCOLS = ['http:', 'https:'];\n\nexport function validateConfig(config: AlterConnectConfig): void {\n if (config.baseURL !== undefined) {\n // Capture into a locally-typed const so the unreachable block below\n // still type-checks after the early throw (TS loses the narrowing\n // from `config.baseURL !== undefined` across the throw boundary).\n const baseURL: string = config.baseURL;\n\n // --- REMOVE_TO_ENABLE_BASE_URL_OVERRIDE ---\n // Delete this throw to enable baseURL override support. The deep\n // validation below is ready to accept any well-formed http/https\n // URL without userinfo, query, or fragment. Keep the throw message\n // free of the supplied value — callers may pass strings carrying\n // secrets, and thrown errors flow into error-tracking integrations.\n throw new Error(\n 'AlterConnectConfig.baseURL is reserved and not yet supported. Omit the field to use the production Alter host.'\n );\n // --- END REMOVE_TO_ENABLE_BASE_URL_OVERRIDE ---\n\n // Everything below is unreachable today (guarded by the throw above)\n // but preserved so re-enabling the override is a one-line change.\n // `eslint-disable-next-line` only silences the one following\n // statement, so every subsequent unreachable statement in this\n // block would re-trigger `no-unreachable`. Wrap the whole block\n // with an enable/disable pair instead.\n /* eslint-disable no-unreachable */\n if (typeof baseURL !== 'string' || baseURL.trim() === '') {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: must be a non-empty string. Omit the field to use the default Alter host.'\n );\n }\n if (baseURL !== baseURL.trim()) {\n // URL parsers silently trim ASCII whitespace but we store the raw\n // string on the instance and string-concat it into popup URLs, so\n // a padded value produces malformed popups. Reject loudly instead.\n //\n // Intentionally do NOT echo the offending value — a misconfigured\n // string could sit next to secret material, and thrown errors flow\n // into error-tracking integrations (Sentry, Datadog RUM, etc.).\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: contains leading or trailing whitespace.'\n );\n }\n let parsed: URL;\n try {\n parsed = new URL(baseURL);\n } catch {\n // Do NOT echo baseURL — a malformed string can still carry secret\n // material (e.g. query tokens that slipped past the whitespace\n // check) that would flow into error trackers via the thrown\n // message. The error tells the operator which rule was violated;\n // they can inspect their own config to debug.\n throw new Error(\n `Invalid AlterConnectConfig.baseURL: must be a full URL with protocol (e.g., \"${DEFAULT_ALTER_HOST}\").`\n );\n }\n // Reject every field that could carry secret material BEFORE any\n // downstream code echoes the URL string. userinfo = inline basic-auth\n // credentials, search = query-string tokens, hash = session tokens.\n // A base URL is just a host (+ optional path prefix) by definition —\n // if the operator needs any of these, they're passing the wrong value.\n if (parsed.username !== '' || parsed.password !== '') {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: credentials embedded in the URL (user:password@) are not allowed. Remove them from the URL and provide credentials through the normal auth flow.'\n );\n }\n if (parsed.search !== '') {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: query strings are not allowed on a base URL. Remove the \"?...\" portion.'\n );\n }\n if (parsed.hash !== '') {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: URL fragments are not allowed on a base URL. Remove the \"#...\" portion.'\n );\n }\n if (!ALLOWED_BASE_URL_PROTOCOLS.includes(parsed.protocol)) {\n // Do NOT echo baseURL — at this point we've rejected userinfo,\n // query, and fragment, so the remaining secret-leak risk is low,\n // but echoing is inconsistent with the other validation branches\n // and isn't needed to convey the rule to the operator.\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: only http:// and https:// are allowed.'\n );\n }\n /* eslint-enable no-unreachable */\n }\n}\n\n/**\n * Merge user config with defaults\n */\nexport function mergeConfig(config: AlterConnectConfig): FullConfig {\n return {\n debug: config.debug ?? false,\n };\n}\n\n/**\n * Debug logging helper\n */\nexport function debugLog(config: FullConfig, ...args: any[]): void {\n if (config.debug) {\n console.log('[Alter Connect]', ...args);\n }\n}\n\n/**\n * Export FullConfig type for use in other modules\n */\nexport type { FullConfig };\n","/**\n * Simple event emitter for SDK events\n */\n\ntype EventHandler = (...args: any[]) => void;\n\n/**\n * Event emitter for SDK internal events\n */\nexport class EventEmitter {\n private events: Map<string, Set<EventHandler>>;\n\n constructor() {\n this.events = new Map();\n }\n\n /**\n * Register an event listener\n * @returns Unsubscribe function\n */\n on(event: string, handler: EventHandler): () => void {\n if (!this.events.has(event)) {\n this.events.set(event, new Set());\n }\n\n this.events.get(event)!.add(handler);\n\n // Return unsubscribe function\n return () => this.off(event, handler);\n }\n\n /**\n * Remove an event listener\n */\n off(event: string, handler: EventHandler): void {\n const handlers = this.events.get(event);\n if (handlers) {\n handlers.delete(handler);\n }\n }\n\n /**\n * Emit an event\n */\n emit(event: string, ...args: any[]): void {\n const handlers = this.events.get(event);\n if (handlers) {\n handlers.forEach(handler => {\n try {\n handler(...args);\n } catch (error) {\n console.error(`[Alter Connect] Error in event handler for '${event}':`, error);\n }\n });\n }\n }\n\n /**\n * Remove all listeners for an event, or all listeners if no event specified\n */\n removeAllListeners(event?: string): void {\n if (event) {\n this.events.delete(event);\n } else {\n this.events.clear();\n }\n }\n}\n","/**\n * State management for SDK\n */\n\nimport type { AlterError } from '../types';\n\n/**\n * SDK internal state\n */\nexport interface SDKState {\n isOpen: boolean;\n sessionToken: string | null;\n error: AlterError | null;\n}\n\ntype StateListener = (state: SDKState) => void;\n\n/**\n * Simple state manager\n */\nexport class StateManager {\n private state: SDKState;\n private listeners: Set<StateListener>;\n\n constructor() {\n this.state = {\n isOpen: false,\n sessionToken: null,\n error: null,\n };\n this.listeners = new Set();\n }\n\n /**\n * Get current state\n */\n getState(): Readonly<SDKState> {\n return { ...this.state };\n }\n\n /**\n * Get a specific state value\n */\n get<K extends keyof SDKState>(key: K): SDKState[K] {\n return this.state[key];\n }\n\n /**\n * Update state\n */\n setState(updates: Partial<SDKState>): void {\n this.state = { ...this.state, ...updates };\n this.notifyListeners();\n }\n\n /**\n * Subscribe to state changes\n */\n subscribe(listener: StateListener): () => void {\n this.listeners.add(listener);\n\n // Return unsubscribe function\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Clear all listeners\n */\n clearListeners(): void {\n this.listeners.clear();\n }\n\n /**\n * Notify all listeners of state change\n */\n private notifyListeners(): void {\n const currentState = this.getState();\n this.listeners.forEach(listener => {\n try {\n listener(currentState);\n } catch (error) {\n console.error('[Alter Connect] Error in state listener:', error);\n }\n });\n }\n}\n","/**\n * Mobile device detection and responsive utilities\n */\n\n/**\n * Detect if the current device is a mobile device\n */\nexport function isMobileDevice(): boolean {\n const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;\n\n // Check for mobile patterns\n const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;\n const isMobileUA = mobileRegex.test(userAgent);\n\n // Check for touch capability\n const hasTouchScreen = ('ontouchstart' in window) ||\n (navigator.maxTouchPoints > 0) ||\n ((navigator as any).msMaxTouchPoints > 0);\n\n // Check screen size\n const isSmallScreen = window.innerWidth <= 768;\n\n return isMobileUA || (hasTouchScreen && isSmallScreen);\n}\n\n/**\n * Detect if the device is specifically a phone (vs tablet)\n */\nexport function isPhoneDevice(): boolean {\n return isMobileDevice() && window.innerWidth <= 480;\n}\n\n/**\n * Detect if the device is a tablet\n */\nexport function isTabletDevice(): boolean {\n return isMobileDevice() && window.innerWidth > 480 && window.innerWidth <= 1024;\n}\n\n/**\n * Get the device type\n */\nexport function getDeviceType(): 'desktop' | 'tablet' | 'phone' {\n if (isPhoneDevice()) return 'phone';\n if (isTabletDevice()) return 'tablet';\n return 'desktop';\n}\n\n/**\n * Check if we should use redirect flow instead of popup\n *\n * Popup windows don't work well on mobile:\n * - Browsers often block them\n * - They open as full-screen tabs\n * - Users lose context of parent app\n */\nexport function shouldUseRedirectFlow(): boolean {\n const deviceType = getDeviceType();\n\n // Always use redirect for phones\n if (deviceType === 'phone') return true;\n\n // Use redirect for tablets in portrait mode (popups are cramped)\n if (deviceType === 'tablet' && window.innerHeight > window.innerWidth) {\n return true;\n }\n\n // Use popup for desktop and tablets in landscape\n return false;\n}\n","/**\n * OAuth popup and redirect handler\n */\n\nimport type { Grant, AlterError } from '../types';\nimport { DEFAULT_ALTER_HOST } from '../core/config';\nimport { shouldUseRedirectFlow } from '../utils/mobile';\n\ninterface OAuthHandlerOptions {\n baseURL: string;\n onSuccess: (grants: Grant[]) => void;\n onError: (error: AlterError) => void;\n onCancel: () => void;\n popupWidth?: number;\n popupHeight?: number;\n debug?: boolean;\n /** Expected origin for postMessage validation. Derived from the OAuth URL. */\n expectedOrigin?: string;\n}\n\n/**\n * Parse an operation string into a valid Grant['operation'] value\n */\nfunction parseOperation(value: string | null | undefined): Grant['operation'] {\n if (value === 'reauth') return 'reauth';\n return 'creation';\n}\n\n/**\n * Parse a status string into a valid Grant['status'] value\n */\nfunction parseStatus(value: string | null | undefined): Grant['status'] {\n if (value === 'pending') return 'pending';\n if (value === 'error') return 'error';\n return 'active';\n}\n\n/**\n * Clean up OAuth redirect state and restore the original URL\n */\nfunction cleanupRedirectState(returnUrl: string): void {\n sessionStorage.removeItem('alter_oauth_state');\n try {\n const cleanUrl = new URL(returnUrl);\n window.history.replaceState({}, document.title, cleanUrl.pathname + cleanUrl.search + cleanUrl.hash);\n } catch {\n // If returnUrl is malformed, just remove query params from current URL\n window.history.replaceState({}, document.title, window.location.pathname);\n }\n}\n\n/**\n * Handles OAuth flow in popup window\n */\nexport class OAuthHandler {\n private popup: Window | null = null;\n private pollInterval: number | null = null;\n private messageListener: ((event: MessageEvent) => void) | null = null;\n private options: Required<OAuthHandlerOptions>;\n private expectedOrigin: string;\n private settled: boolean = false;\n\n constructor(options: OAuthHandlerOptions) {\n this.options = {\n baseURL: options.baseURL,\n onSuccess: options.onSuccess,\n onError: options.onError,\n onCancel: options.onCancel,\n popupWidth: options.popupWidth || 500,\n popupHeight: options.popupHeight || 700,\n debug: options.debug || false,\n expectedOrigin: options.expectedOrigin || '',\n };\n\n // Extract origin from baseURL for postMessage validation\n try {\n this.expectedOrigin = new URL(options.baseURL).origin;\n } catch {\n throw new Error(\n `Invalid baseURL: \"${options.baseURL}\". Must be a full URL with protocol (e.g., \"${DEFAULT_ALTER_HOST}\").`\n );\n }\n }\n\n /**\n * Start OAuth flow (automatically chooses popup or redirect based on device)\n */\n startOAuth(url: string): void {\n // Derive expected origin from the OAuth URL if not explicitly set.\n // SECURITY: Fail-closed — if we can't determine origin, abort the flow.\n if (!this.options.expectedOrigin) {\n try {\n const parsed = new URL(url);\n this.options.expectedOrigin = parsed.origin;\n this.log('Derived expected origin:', this.options.expectedOrigin);\n } catch {\n this.log('Failed to parse OAuth URL for origin validation:', url);\n this.options.onError({\n code: 'invalid_oauth_url',\n message: 'Failed to determine origin from OAuth URL. Cannot proceed securely.',\n });\n return;\n }\n }\n\n if (shouldUseRedirectFlow()) {\n this.log('Using redirect flow for mobile device');\n this.startRedirectFlow(url);\n } else {\n this.log('Using popup flow for desktop');\n this.openPopup(url);\n }\n }\n\n /**\n * Start OAuth redirect flow (for mobile)\n * Saves state and redirects the entire page\n */\n private startRedirectFlow(url: string): void {\n this.log('Starting redirect flow:', url);\n\n // Save state to sessionStorage to restore after redirect\n const state = {\n timestamp: Date.now(),\n returnUrl: window.location.href,\n };\n\n try {\n sessionStorage.setItem('alter_oauth_state', JSON.stringify(state));\n } catch (error) {\n this.log('Failed to save state:', error);\n this.options.onError({\n code: 'redirect_error',\n message: 'Failed to start OAuth flow: could not save session state',\n details: { error },\n });\n return;\n }\n\n // Redirect to OAuth URL (full page redirect)\n window.location.href = url;\n }\n\n /**\n * Check if returning from OAuth redirect and handle result\n * Call this on page load to detect OAuth return\n *\n * @returns true if OAuth return was detected and handled\n */\n static checkOAuthReturn(\n onSuccess: (grants: Grant[]) => void,\n onError: (error: AlterError) => void\n ): boolean {\n // Check if we have OAuth state saved\n const stateJson = sessionStorage.getItem('alter_oauth_state');\n if (!stateJson) {\n return false;\n }\n\n try {\n const state = JSON.parse(stateJson);\n\n // Check if this is a valid OAuth return (within 5 minutes)\n const elapsed = Date.now() - state.timestamp;\n if (elapsed > 5 * 60 * 1000) {\n // Stale state, clean up\n sessionStorage.removeItem('alter_oauth_state');\n return false;\n }\n\n // Check URL for OAuth callback parameters\n const urlParams = new URLSearchParams(window.location.search);\n const success = urlParams.get('alter_connect_success');\n const error = urlParams.get('alter_connect_error');\n\n if (success === 'true') {\n // Validate required fields\n const grantId = urlParams.get('grant_id');\n const provider = urlParams.get('provider');\n const accountIdentifier = urlParams.get('account_identifier');\n\n if (!grantId || !provider || !accountIdentifier) {\n cleanupRedirectState(state.returnUrl);\n onError({\n code: 'invalid_response',\n message: 'OAuth redirect returned incomplete grant data',\n });\n return true;\n }\n\n const grant: Grant = {\n grant_id: grantId,\n provider: provider,\n provider_name: urlParams.get('provider_name') || provider,\n account_identifier: accountIdentifier,\n timestamp: urlParams.get('timestamp') || new Date().toISOString(),\n operation: parseOperation(urlParams.get('operation')),\n scopes: urlParams.get('scopes')?.split(',') || [],\n status: parseStatus(urlParams.get('status')),\n };\n\n cleanupRedirectState(state.returnUrl);\n onSuccess([grant]); // Wrap in array for unified interface\n return true;\n }\n\n if (error) {\n const alterError: AlterError = {\n code: urlParams.get('error_code') || 'oauth_error',\n message: urlParams.get('error_description') || 'OAuth authorization failed',\n };\n\n cleanupRedirectState(state.returnUrl);\n onError(alterError);\n return true;\n }\n\n return false;\n } catch (err) {\n console.error('[OAuth Handler] Failed to check OAuth return:', err);\n sessionStorage.removeItem('alter_oauth_state');\n return false;\n }\n }\n\n /**\n * Open OAuth popup (for desktop)\n */\n openPopup(url: string): void {\n // Calculate popup position (centered)\n const left = window.screenX + (window.outerWidth - this.options.popupWidth) / 2;\n const top = window.screenY + (window.outerHeight - this.options.popupHeight) / 2;\n\n const features = [\n `width=${this.options.popupWidth}`,\n `height=${this.options.popupHeight}`,\n `left=${left}`,\n `top=${top}`,\n 'resizable=yes',\n 'scrollbars=yes',\n 'status=yes',\n ].join(',');\n\n this.log('Opening OAuth popup:', url);\n\n // Open popup\n this.popup = window.open(url, 'alter_oauth_popup', features);\n\n if (!this.popup) {\n this.options.onError({\n code: 'popup_blocked',\n message: 'Popup was blocked by browser. Please allow popups for this site.',\n });\n return;\n }\n\n // Start monitoring popup\n this.startPolling();\n this.setupMessageListener();\n }\n\n /**\n * Close popup and clean up\n */\n close(): void {\n this.log('Closing OAuth handler');\n\n // Close popup\n if (this.popup && !this.popup.closed) {\n this.popup.close();\n }\n this.popup = null;\n\n // Stop polling\n if (this.pollInterval !== null) {\n clearInterval(this.pollInterval);\n this.pollInterval = null;\n }\n\n // Remove message listener\n if (this.messageListener) {\n window.removeEventListener('message', this.messageListener);\n this.messageListener = null;\n }\n }\n\n /**\n * Poll to detect when popup is closed\n */\n private startPolling(): void {\n this.pollInterval = window.setInterval(() => {\n if (this.settled) return;\n if (!this.popup || this.popup.closed) {\n this.log('Popup closed by user');\n this.settled = true;\n this.close();\n this.options.onCancel();\n }\n }, 500);\n }\n\n /**\n * Listen for postMessage from OAuth callback page\n */\n private setupMessageListener(): void {\n this.messageListener = (event: MessageEvent) => {\n // Ignore if already settled (prevents race with polling)\n if (this.settled) return;\n\n // Validate origin against the expected backend origin\n if (event.origin !== this.expectedOrigin) {\n this.log('Rejected message from unexpected origin:', event.origin, '(expected:', this.expectedOrigin + ')');\n return;\n }\n\n this.log('Received message from', event.origin);\n\n const data = event.data;\n\n // Check message type\n if (data && typeof data === 'object') {\n if (data.type === 'alter_connect_success') {\n this.log('OAuth success');\n\n // Accept both grants array and legacy connections array\n const rawGrants = Array.isArray(data.grants) ? data.grants : null;\n if (rawGrants) {\n this.log('Multi-provider success:', rawGrants.length, 'grants');\n const grants: Grant[] = [];\n for (const item of rawGrants) {\n const grantId = item.grant_id as string | undefined;\n if (!grantId || !item.provider) {\n this.log('Skipping invalid grant item:', item);\n continue;\n }\n grants.push({\n grant_id: grantId,\n provider: item.provider as string,\n provider_name: (item.provider_name as string) || (item.provider as string),\n account_identifier: (item.account_identifier as string) || '',\n timestamp: (item.timestamp as string) || new Date().toISOString(),\n operation: parseOperation(item.operation as string | undefined),\n scopes: Array.isArray(item.scopes) ? item.scopes as string[] : [],\n status: parseStatus(item.status as string | undefined),\n metadata: item.metadata as Grant['metadata'],\n });\n }\n\n if (grants.length === 0) {\n this.settled = true;\n this.close();\n this.options.onError({\n code: 'invalid_response',\n message: 'Server returned empty grants array',\n });\n return;\n }\n\n this.settled = true;\n this.close();\n this.options.onSuccess(grants);\n return;\n }\n\n // No valid grants array found\n this.settled = true;\n this.close();\n this.options.onError({\n code: 'invalid_response',\n message: 'Server returned success without grants array',\n });\n } else if (data.type === 'alter_connect_error') {\n this.log('OAuth error');\n\n const error: AlterError = {\n code: (data.error as string) || 'oauth_error',\n message: (data.error_description as string) || 'OAuth authorization failed',\n details: data as Record<string, unknown>,\n };\n\n this.settled = true;\n this.close();\n this.options.onError(error);\n }\n }\n };\n\n window.addEventListener('message', this.messageListener);\n }\n\n /**\n * Log debug message\n */\n private log(...args: unknown[]): void {\n if (this.options.debug) {\n console.log('[OAuth Handler]', ...args);\n }\n }\n}\n","/**\n * Main AlterConnect SDK class\n *\n * Opens the backend-served Connect UI in a popup. The backend handles\n * provider selection, branding, and OAuth — then sends the result back via postMessage.\n */\n\nimport type { AlterConnectConfig, OpenOptions, Grant, AlterError } from '../types';\nimport { validateConfig, mergeConfig, debugLog, DEFAULT_ALTER_HOST, type FullConfig } from './config';\nimport { EventEmitter } from './events';\nimport { StateManager } from '../state/manager';\nimport { OAuthHandler } from '../oauth/handler';\n\n/**\n * SDK version\n */\nconst VERSION = '0.2.0';\n\n/**\n * Module-scoped flag so the non-default-host warning fires at most once\n * per process. Reset is intentionally not exposed — the warning exists to\n * surface accidental misconfigurations during dev, not to spam logs.\n */\nlet hasWarnedNonDefaultHost = false;\n\n/**\n * Main SDK class\n */\nexport class AlterConnect {\n private config: FullConfig;\n private eventEmitter: EventEmitter;\n private stateManager: StateManager;\n private _isInitialized: boolean;\n private _oauthHandler: OAuthHandler | null = null;\n private readonly _baseURL: string;\n private _perOpenCleanups: Array<() => void> = [];\n\n /**\n * Private constructor (use AlterConnect.create() instead)\n */\n private constructor(config: AlterConnectConfig = {}) {\n // Validate and merge config\n validateConfig(config);\n this.config = mergeConfig(config);\n\n // Resolve the Alter host once at construction time. `baseURL` is an\n // environment-level setting (production vs self-hosted vs local-dev),\n // not a per-`open()` decision — that's why it lives on the constructor\n // config and not on OpenOptions.\n this._baseURL = (config.baseURL ?? DEFAULT_ALTER_HOST).replace(/\\/+$/, '');\n if (this._baseURL !== DEFAULT_ALTER_HOST && !hasWarnedNonDefaultHost) {\n hasWarnedNonDefaultHost = true;\n // Log only the origin, not the full URL. validateConfig rejects\n // userinfo/search/hash, so the path is the only \"extra\" piece the\n // origin strips — but it costs us nothing to be defensive here in\n // case validateConfig's allowlist is ever broadened. The warn\n // message is forwarded to customer-side error trackers (Sentry,\n // Datadog RUM), so minimizing what flows through it is prudent.\n const origin = new URL(this._baseURL).origin;\n console.warn(\n `[alter-connect] Using non-default Alter host: ${origin}. ` +\n 'Unset baseURL in AlterConnect.create() for production.'\n );\n }\n\n // Initialize components\n this.eventEmitter = new EventEmitter();\n this.stateManager = new StateManager();\n this._isInitialized = true;\n\n debugLog(this.config, 'Alter Connect SDK initialized', { version: VERSION, baseURL: this._baseURL });\n\n // Check if returning from OAuth redirect (mobile flow)\n this.checkRedirectReturn();\n }\n\n /**\n * Check if returning from OAuth redirect and handle result\n * This runs automatically on SDK initialization\n */\n private checkRedirectReturn(): void {\n const handled = OAuthHandler.checkOAuthReturn(\n (grants: Grant[]) => {\n debugLog(this.config, 'OAuth redirect return - success:', grants);\n // Emit success event (will be picked up by event handlers if SDK was re-initialized)\n this.eventEmitter.emit('success', grants);\n },\n (error: AlterError) => {\n debugLog(this.config, 'OAuth redirect return - error:', error);\n // Emit error event\n this.eventEmitter.emit('error', error);\n }\n );\n\n if (handled) {\n debugLog(this.config, 'OAuth redirect return detected and handled');\n }\n }\n\n /**\n * Create a new AlterConnect instance\n *\n * @param config Optional configuration\n * @returns AlterConnect instance\n *\n * @example\n * const alterConnect = AlterConnect.create();\n * const alterConnect = AlterConnect.create({ debug: true });\n */\n static create(config?: AlterConnectConfig): AlterConnect {\n return new AlterConnect(config);\n }\n\n /**\n * Open the Connect UI in a popup window\n *\n * Opens the backend-served Connect UI at {baseURL}/oauth/connect#session={token}.\n * The backend handles provider selection, branding, and OAuth flow.\n * On success, the backend sends a postMessage back to this window.\n *\n * @param options Configuration including the session token from your backend\n *\n * @example\n * // 1. Get session token from YOUR backend\n * const { session_token } = await fetch('/api/alter/create-session').then(r => r.json());\n *\n * // 2. Open Connect UI\n * await alterConnect.open({\n * token: session_token,\n * onSuccess: (grants) => {\n * console.log('Connected!', grants);\n * },\n * onError: (error) => {\n * console.error('Failed:', error);\n * }\n * });\n */\n async open(options: OpenOptions): Promise<void> {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call open() - SDK instance has been destroyed');\n }\n\n debugLog(this.config, 'Opening Connect UI');\n\n // Validate required options\n if (!options.token || typeof options.token !== 'string') {\n throw this.createError('invalid_options', 'Session token is required. Create one from your backend using POST /sdk/oauth/connect/session');\n }\n\n if (!options.onSuccess || typeof options.onSuccess !== 'function') {\n throw this.createError('invalid_options', 'onSuccess callback is required');\n }\n\n // Check if already open\n if (this.isOpen()) {\n debugLog(this.config, 'Connect UI is already open');\n return;\n }\n\n // Create OAuth handler first — this validates baseURL and throws on invalid input.\n // We do this BEFORE setting isOpen to avoid leaving the SDK in a broken state.\n this._oauthHandler = new OAuthHandler({\n baseURL: this._baseURL,\n onSuccess: (grants: Grant[]) => {\n debugLog(this.config, 'OAuth success:', grants);\n this.handleOAuthSuccess(grants);\n },\n onError: (error: AlterError) => {\n debugLog(this.config, 'OAuth error:', error);\n this.handleOAuthError(error);\n },\n onCancel: () => {\n debugLog(this.config, 'OAuth cancelled (popup closed)');\n this.handleOAuthCancel();\n },\n popupWidth: 500,\n popupHeight: 700,\n debug: this.config.debug,\n });\n\n // Update state (after handler creation succeeds)\n this.stateManager.setState({\n isOpen: true,\n error: null,\n sessionToken: options.token,\n });\n\n // Register event handlers\n this.registerEventHandlers(options);\n\n // Build the Connect UI URL — the backend serves the full UI\n const connectURL = `${this._baseURL}/sdk/oauth/connect#session=${encodeURIComponent(options.token)}`;\n\n debugLog(this.config, 'Connect URL:', connectURL);\n\n // Open the Connect UI (popup or redirect depending on device)\n this._oauthHandler.startOAuth(connectURL);\n\n // Emit analytics event\n if (options.onEvent) {\n options.onEvent('connect_opened', {\n timestamp: new Date().toISOString(),\n });\n }\n\n debugLog(this.config, 'Connect UI opened successfully');\n }\n\n /**\n * Close the Connect UI\n */\n close(): void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call close() - SDK instance has been destroyed');\n }\n\n debugLog(this.config, 'Closing Connect UI');\n\n // Clean up OAuth handler if active\n if (this._oauthHandler) {\n this._oauthHandler.close();\n this._oauthHandler = null;\n }\n\n // Remove only per-open handlers (preserves global handlers from on())\n this._perOpenCleanups.forEach(cleanup => cleanup());\n this._perOpenCleanups = [];\n\n // Update state\n this.stateManager.setState({ isOpen: false });\n\n this.eventEmitter.emit('close');\n }\n\n /**\n * Destroy the SDK instance and clean up resources\n */\n destroy(): void {\n if (!this._isInitialized) {\n // Already destroyed, silently return\n return;\n }\n\n debugLog(this.config, 'Destroying SDK instance');\n\n // Clean up OAuth handler if active\n if (this._oauthHandler) {\n this._oauthHandler.close();\n this._oauthHandler = null;\n }\n\n // Update state\n if (this.stateManager.get('isOpen')) {\n this.stateManager.setState({ isOpen: false });\n }\n\n // Clean up resources\n this.eventEmitter.removeAllListeners();\n this.stateManager.clearListeners();\n this._isInitialized = false;\n }\n\n /**\n * Register an event listener\n */\n on(event: string, handler: (...args: any[]) => void): () => void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call on() - SDK instance has been destroyed');\n }\n\n return this.eventEmitter.on(event, handler);\n }\n\n /**\n * Remove an event listener\n */\n off(event: string, handler: (...args: any[]) => void): void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call off() - SDK instance has been destroyed');\n }\n\n this.eventEmitter.off(event, handler);\n }\n\n /**\n * Check if Connect UI is open\n */\n isOpen(): boolean {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call isOpen() - SDK instance has been destroyed');\n }\n\n return this.stateManager.get('isOpen');\n }\n\n /**\n * Get SDK version\n */\n getVersion(): string {\n return VERSION;\n }\n\n /**\n * Clean up OAuth handler reference after a flow completes.\n * The OAuthHandler.close() is already called internally by the handler\n * before invoking callbacks, so we only need to null the reference here.\n */\n private cleanupHandler(): void {\n this._oauthHandler = null;\n }\n\n /**\n * Handle OAuth success\n */\n private handleOAuthSuccess(grants: Grant[]): void {\n this.cleanupHandler();\n this.eventEmitter.emit('success', grants);\n }\n\n /**\n * Handle OAuth error\n */\n private handleOAuthError(error: AlterError): void {\n this.cleanupHandler();\n this.stateManager.setState({ error });\n this.eventEmitter.emit('error', error);\n }\n\n /**\n * Handle OAuth cancellation (user closed popup)\n */\n private handleOAuthCancel(): void {\n this.cleanupHandler();\n this.eventEmitter.emit('exit');\n }\n\n /**\n * Register event handlers from OpenOptions\n */\n private registerEventHandlers(options: OpenOptions): void {\n // Success handler\n if (options.onSuccess) {\n this._perOpenCleanups.push(\n this.eventEmitter.on('success', (grants: Grant[]) => {\n options.onSuccess(grants);\n this.close();\n })\n );\n }\n\n // Exit handler\n if (options.onExit) {\n this._perOpenCleanups.push(\n this.eventEmitter.on('exit', () => {\n options.onExit!();\n this.close();\n })\n );\n }\n\n // Error handler — always registered to ensure close() is called\n this._perOpenCleanups.push(\n this.eventEmitter.on('error', (error: AlterError) => {\n if (options.onError) {\n options.onError(error);\n }\n this.close();\n })\n );\n\n // Event handler (analytics)\n if (options.onEvent) {\n this._perOpenCleanups.push(\n this.eventEmitter.on('event', (eventName: string, metadata: any) => {\n options.onEvent!(eventName, metadata);\n })\n );\n }\n }\n\n /**\n * Create an AlterError object\n */\n private createError(code: string, message: string, details?: Record<string, any>): Error {\n const error = new Error(message);\n (error as any).code = code;\n (error as any).details = details;\n return error;\n }\n}\n"],"names":["DEFAULT_ALTER_HOST","debugLog","config","args","debug","console","log","EventEmitter","constructor","this","events","Map","on","event","handler","has","set","Set","get","add","off","handlers","delete","emit","forEach","error","removeAllListeners","clear","StateManager","state","isOpen","sessionToken","listeners","getState","key","setState","updates","notifyListeners","subscribe","listener","clearListeners","currentState","isMobileDevice","userAgent","navigator","vendor","window","opera","isMobileUA","test","hasTouchScreen","maxTouchPoints","msMaxTouchPoints","isSmallScreen","innerWidth","getDeviceType","parseOperation","value","parseStatus","cleanupRedirectState","returnUrl","sessionStorage","removeItem","cleanUrl","URL","history","replaceState","document","title","pathname","search","hash","location","OAuthHandler","options","popup","pollInterval","messageListener","settled","baseURL","onSuccess","onError","onCancel","popupWidth","popupHeight","expectedOrigin","origin","Error","startOAuth","url","parsed","code","message","deviceType","innerHeight","shouldUseRedirectFlow","openPopup","startRedirectFlow","timestamp","Date","now","href","setItem","JSON","stringify","details","checkOAuthReturn","stateJson","getItem","parse","urlParams","URLSearchParams","success","grantId","provider","accountIdentifier","grant","grant_id","provider_name","account_identifier","toISOString","operation","scopes","split","status","alterError","err","left","screenX","outerWidth","top","screenY","outerHeight","features","join","open","startPolling","setupMessageListener","close","closed","clearInterval","removeEventListener","setInterval","data","type","rawGrants","Array","isArray","grants","length","item","push","metadata","error_description","addEventListener","VERSION","hasWarnedNonDefaultHost","AlterConnect","_oauthHandler","_perOpenCleanups","undefined","validateConfig","mergeConfig","_baseURL","replace","warn","eventEmitter","stateManager","_isInitialized","version","checkRedirectReturn","create","createError","token","handleOAuthSuccess","handleOAuthError","handleOAuthCancel","registerEventHandlers","connectURL","encodeURIComponent","onEvent","cleanup","destroy","getVersion","cleanupHandler","onExit","eventName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"alter-connect.esm.js","sources":["../src/core/config.ts","../src/core/events.ts","../src/state/manager.ts","../src/utils/mobile.ts","../src/oauth/handler.ts","../src/core/alter-connect.ts"],"sourcesContent":["/**\n * Configuration validation and management\n */\n\nimport type { AlterConnectConfig } from \"../types\";\n\n/**\n * Canonical Alter Connect host.\n *\n * Single source of truth for the widget's default host. `alter-connect.ts`\n * uses it as the constructor fallback; `config.ts` and `oauth/handler.ts`\n * surface it in error messages so operators always see the same example\n * URL no matter which validation layer flags their misconfig.\n */\nexport const DEFAULT_ALTER_HOST = \"https://backend.alterauth.com\";\n\n/**\n * Internal config used by the SDK\n */\ninterface FullConfig {\n debug: boolean;\n}\n\n/**\n * Validate configuration.\n *\n * `baseURL` is RESERVED in the public type for future self-hosted /\n * non-production support. It is not yet accepted at runtime — see the\n * `REMOVE_TO_ENABLE_BASE_URL_OVERRIDE` block below. The deep validation\n * beneath it is intentionally kept so re-enabling is a single-line\n * deletion; each check retains its inline rationale so future-readers\n * don't have to reconstruct the reasoning.\n */\nconst ALLOWED_BASE_URL_PROTOCOLS = [\"http:\", \"https:\"];\n\nexport function validateConfig(config: AlterConnectConfig): void {\n if (config.baseURL !== undefined) {\n // Capture into a locally-typed const so the unreachable block below\n // still type-checks after the early throw (TS loses the narrowing\n // from `config.baseURL !== undefined` across the throw boundary).\n const baseURL: string = config.baseURL;\n\n // --- REMOVE_TO_ENABLE_BASE_URL_OVERRIDE ---\n // Delete this throw to enable baseURL override support. The deep\n // validation below is ready to accept any well-formed http/https\n // URL without userinfo, query, or fragment. Keep the throw message\n // free of the supplied value — callers may pass strings carrying\n // secrets, and thrown errors flow into error-tracking integrations.\n throw new Error(\n \"AlterConnectConfig.baseURL is reserved and not yet supported. Omit the field to use the production Alter host.\",\n );\n // --- END REMOVE_TO_ENABLE_BASE_URL_OVERRIDE ---\n\n // Everything below is unreachable today (guarded by the throw above)\n // but preserved so re-enabling the override is a one-line change.\n // This package enables `no-unreachable` (unreachable code is almost\n // always a bug), so this ONE deliberate case carries an explicit\n // disable/enable pair — deleting the throw above must also delete it.\n /* eslint-disable no-unreachable */\n if (typeof baseURL !== \"string\" || baseURL.trim() === \"\") {\n throw new Error(\n \"Invalid AlterConnectConfig.baseURL: must be a non-empty string. Omit the field to use the default Alter host.\",\n );\n }\n if (baseURL !== baseURL.trim()) {\n // URL parsers silently trim ASCII whitespace but we store the raw\n // string on the instance and string-concat it into popup URLs, so\n // a padded value produces malformed popups. Reject loudly instead.\n //\n // Intentionally do NOT echo the offending value — a misconfigured\n // string could sit next to secret material, and thrown errors flow\n // into error-tracking integrations (Sentry, Datadog RUM, etc.).\n throw new Error(\n \"Invalid AlterConnectConfig.baseURL: contains leading or trailing whitespace.\",\n );\n }\n let parsed: URL;\n try {\n parsed = new URL(baseURL);\n } catch {\n // Do NOT echo baseURL — a malformed string can still carry secret\n // material (e.g. query tokens that slipped past the whitespace\n // check) that would flow into error trackers via the thrown\n // message. The error tells the operator which rule was violated;\n // they can inspect their own config to debug.\n throw new Error(\n `Invalid AlterConnectConfig.baseURL: must be a full URL with protocol (e.g., \"${DEFAULT_ALTER_HOST}\").`,\n );\n }\n // Reject every field that could carry secret material BEFORE any\n // downstream code echoes the URL string. userinfo = inline basic-auth\n // credentials, search = query-string tokens, hash = session tokens.\n // A base URL is just a host (+ optional path prefix) by definition —\n // if the operator needs any of these, they're passing the wrong value.\n if (parsed.username !== \"\" || parsed.password !== \"\") {\n throw new Error(\n \"Invalid AlterConnectConfig.baseURL: credentials embedded in the URL (user:password@) are not allowed. Remove them from the URL and provide credentials through the normal auth flow.\",\n );\n }\n if (parsed.search !== \"\") {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: query strings are not allowed on a base URL. Remove the \"?...\" portion.',\n );\n }\n if (parsed.hash !== \"\") {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: URL fragments are not allowed on a base URL. Remove the \"#...\" portion.',\n );\n }\n if (!ALLOWED_BASE_URL_PROTOCOLS.includes(parsed.protocol)) {\n // Do NOT echo baseURL — at this point we've rejected userinfo,\n // query, and fragment, so the remaining secret-leak risk is low,\n // but echoing is inconsistent with the other validation branches\n // and isn't needed to convey the rule to the operator.\n throw new Error(\n \"Invalid AlterConnectConfig.baseURL: only http:// and https:// are allowed.\",\n );\n }\n /* eslint-enable no-unreachable */\n }\n}\n\n/**\n * Merge user config with defaults\n */\nexport function mergeConfig(config: AlterConnectConfig): FullConfig {\n return {\n debug: config.debug ?? false,\n };\n}\n\n/**\n * Debug logging helper\n */\nexport function debugLog(config: FullConfig, ...args: unknown[]): void {\n if (config.debug) {\n // Deliberate console.log: this IS the debug logger, and it only fires\n // when the integrator explicitly opts in via ``config.debug``.\n // eslint-disable-next-line no-console\n console.log(\"[Alter Connect]\", ...args);\n }\n}\n\n/**\n * Export FullConfig type for use in other modules\n */\nexport type { FullConfig };\n","/**\n * Simple event emitter for SDK events\n */\n\n// Variadic ``never[]`` keeps registration contravariant-safe under\n// strictFunctionTypes: any concretely-typed listener (e.g.\n// ``(name: string) => void``) is assignable, while the emitter itself\n// invokes handlers through an ``unknown[]`` cast in emit().\nexport type EventHandler = (...args: never[]) => void;\n\n/**\n * Event emitter for SDK internal events\n */\nexport class EventEmitter {\n private events: Map<string, Set<EventHandler>>;\n\n constructor() {\n this.events = new Map();\n }\n\n /**\n * Register an event listener\n * @returns Unsubscribe function\n */\n on(event: string, handler: EventHandler): () => void {\n if (!this.events.has(event)) {\n this.events.set(event, new Set());\n }\n\n this.events.get(event)!.add(handler);\n\n // Return unsubscribe function\n return () => this.off(event, handler);\n }\n\n /**\n * Remove an event listener\n */\n off(event: string, handler: EventHandler): void {\n const handlers = this.events.get(event);\n if (handlers) {\n handlers.delete(handler);\n }\n }\n\n /**\n * Emit an event\n */\n emit(event: string, ...args: unknown[]): void {\n const handlers = this.events.get(event);\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n (handler as (...emitted: unknown[]) => void)(...args);\n } catch (error) {\n console.error(\n `[Alter Connect] Error in event handler for '${event}':`,\n error,\n );\n }\n });\n }\n }\n\n /**\n * Remove all listeners for an event, or all listeners if no event specified\n */\n removeAllListeners(event?: string): void {\n if (event) {\n this.events.delete(event);\n } else {\n this.events.clear();\n }\n }\n}\n","/**\n * State management for SDK\n */\n\nimport type { AlterError } from \"../types\";\n\n/**\n * SDK internal state\n */\nexport interface SDKState {\n isOpen: boolean;\n sessionToken: string | null;\n error: AlterError | null;\n}\n\ntype StateListener = (state: SDKState) => void;\n\n/**\n * Simple state manager\n */\nexport class StateManager {\n private state: SDKState;\n private listeners: Set<StateListener>;\n\n constructor() {\n this.state = {\n isOpen: false,\n sessionToken: null,\n error: null,\n };\n this.listeners = new Set();\n }\n\n /**\n * Get current state\n */\n getState(): Readonly<SDKState> {\n return { ...this.state };\n }\n\n /**\n * Get a specific state value\n */\n get<K extends keyof SDKState>(key: K): SDKState[K] {\n return this.state[key];\n }\n\n /**\n * Update state\n */\n setState(updates: Partial<SDKState>): void {\n this.state = { ...this.state, ...updates };\n this.notifyListeners();\n }\n\n /**\n * Subscribe to state changes\n */\n subscribe(listener: StateListener): () => void {\n this.listeners.add(listener);\n\n // Return unsubscribe function\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Clear all listeners\n */\n clearListeners(): void {\n this.listeners.clear();\n }\n\n /**\n * Notify all listeners of state change\n */\n private notifyListeners(): void {\n const currentState = this.getState();\n this.listeners.forEach((listener) => {\n try {\n listener(currentState);\n } catch (error) {\n console.error(\"[Alter Connect] Error in state listener:\", error);\n }\n });\n }\n}\n","/**\n * Mobile device detection and responsive utilities\n */\n\n/**\n * Detect if the current device is a mobile device\n */\nexport function isMobileDevice(): boolean {\n // ``window.opera`` (legacy Opera) and ``navigator.msMaxTouchPoints``\n // (IE/old Edge) are non-standard vendor fields absent from lib.dom —\n // narrow structural casts instead of ``any``.\n const userAgent =\n navigator.userAgent ||\n navigator.vendor ||\n (window as Window & { opera?: string }).opera ||\n \"\";\n\n // Check for mobile patterns\n const mobileRegex =\n /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;\n const isMobileUA = mobileRegex.test(userAgent);\n\n // Check for touch capability\n const hasTouchScreen =\n \"ontouchstart\" in window ||\n navigator.maxTouchPoints > 0 ||\n ((navigator as Navigator & { msMaxTouchPoints?: number })\n .msMaxTouchPoints ?? 0) > 0;\n\n // Check screen size\n const isSmallScreen = window.innerWidth <= 768;\n\n return isMobileUA || (hasTouchScreen && isSmallScreen);\n}\n\n/**\n * Detect if the device is specifically a phone (vs tablet)\n */\nexport function isPhoneDevice(): boolean {\n return isMobileDevice() && window.innerWidth <= 480;\n}\n\n/**\n * Detect if the device is a tablet\n */\nexport function isTabletDevice(): boolean {\n return (\n isMobileDevice() && window.innerWidth > 480 && window.innerWidth <= 1024\n );\n}\n\n/**\n * Get the device type\n */\nexport function getDeviceType(): \"desktop\" | \"tablet\" | \"phone\" {\n if (isPhoneDevice()) return \"phone\";\n if (isTabletDevice()) return \"tablet\";\n return \"desktop\";\n}\n\n/**\n * Check if we should use redirect flow instead of popup\n *\n * Popup windows don't work well on mobile:\n * - Browsers often block them\n * - They open as full-screen tabs\n * - Users lose context of parent app\n */\nexport function shouldUseRedirectFlow(): boolean {\n const deviceType = getDeviceType();\n\n // Always use redirect for phones\n if (deviceType === \"phone\") return true;\n\n // Use redirect for tablets in portrait mode (popups are cramped)\n if (deviceType === \"tablet\" && window.innerHeight > window.innerWidth) {\n return true;\n }\n\n // Use popup for desktop and tablets in landscape\n return false;\n}\n","/**\n * OAuth popup and redirect handler\n */\n\nimport type { Grant, AlterError } from \"../types\";\nimport { DEFAULT_ALTER_HOST } from \"../core/config\";\nimport { shouldUseRedirectFlow } from \"../utils/mobile\";\n\ninterface OAuthHandlerOptions {\n baseURL: string;\n onSuccess: (grants: Grant[]) => void;\n onError: (error: AlterError) => void;\n onCancel: () => void;\n popupWidth?: number;\n popupHeight?: number;\n debug?: boolean;\n /** Expected origin for postMessage validation. Derived from the OAuth URL. */\n expectedOrigin?: string;\n}\n\n/**\n * Parse an operation string into a valid Grant['operation'] value\n */\nfunction parseOperation(value: string | null | undefined): Grant[\"operation\"] {\n if (value === \"reauth\") return \"reauth\";\n return \"creation\";\n}\n\n/**\n * Parse a status string into a valid Grant['status'] value\n */\nfunction parseStatus(value: string | null | undefined): Grant[\"status\"] {\n if (value === \"pending\") return \"pending\";\n if (value === \"error\") return \"error\";\n return \"active\";\n}\n\n/**\n * Clean up OAuth redirect state and restore the original URL\n */\nfunction cleanupRedirectState(returnUrl: string): void {\n sessionStorage.removeItem(\"alter_oauth_state\");\n try {\n const cleanUrl = new URL(returnUrl);\n window.history.replaceState(\n {},\n document.title,\n cleanUrl.pathname + cleanUrl.search + cleanUrl.hash,\n );\n } catch {\n // If returnUrl is malformed, just remove query params from current URL\n window.history.replaceState({}, document.title, window.location.pathname);\n }\n}\n\n/**\n * Handles OAuth flow in popup window\n */\nexport class OAuthHandler {\n private popup: Window | null = null;\n private pollInterval: number | null = null;\n private messageListener: ((event: MessageEvent) => void) | null = null;\n private options: Required<OAuthHandlerOptions>;\n private expectedOrigin: string;\n private settled: boolean = false;\n\n constructor(options: OAuthHandlerOptions) {\n this.options = {\n baseURL: options.baseURL,\n onSuccess: options.onSuccess,\n onError: options.onError,\n onCancel: options.onCancel,\n popupWidth: options.popupWidth || 500,\n popupHeight: options.popupHeight || 700,\n debug: options.debug || false,\n expectedOrigin: options.expectedOrigin || \"\",\n };\n\n // Extract origin from baseURL for postMessage validation\n try {\n this.expectedOrigin = new URL(options.baseURL).origin;\n } catch {\n throw new Error(\n `Invalid baseURL: \"${options.baseURL}\". Must be a full URL with protocol (e.g., \"${DEFAULT_ALTER_HOST}\").`,\n );\n }\n }\n\n /**\n * Start OAuth flow (automatically chooses popup or redirect based on device)\n */\n startOAuth(url: string): void {\n // Derive expected origin from the OAuth URL if not explicitly set.\n // SECURITY: Fail-closed — if we can't determine origin, abort the flow.\n if (!this.options.expectedOrigin) {\n try {\n const parsed = new URL(url);\n this.options.expectedOrigin = parsed.origin;\n this.log(\"Derived expected origin:\", this.options.expectedOrigin);\n } catch {\n this.log(\"Failed to parse OAuth URL for origin validation:\", url);\n this.options.onError({\n code: \"invalid_oauth_url\",\n message:\n \"Failed to determine origin from OAuth URL. Cannot proceed securely.\",\n });\n return;\n }\n }\n\n if (shouldUseRedirectFlow()) {\n this.log(\"Using redirect flow for mobile device\");\n this.startRedirectFlow(url);\n } else {\n this.log(\"Using popup flow for desktop\");\n this.openPopup(url);\n }\n }\n\n /**\n * Start OAuth redirect flow (for mobile)\n * Saves state and redirects the entire page\n */\n private startRedirectFlow(url: string): void {\n this.log(\"Starting redirect flow:\", url);\n\n // Save state to sessionStorage to restore after redirect\n const state = {\n timestamp: Date.now(),\n returnUrl: window.location.href,\n };\n\n try {\n sessionStorage.setItem(\"alter_oauth_state\", JSON.stringify(state));\n } catch (error) {\n this.log(\"Failed to save state:\", error);\n this.options.onError({\n code: \"redirect_error\",\n message: \"Failed to start OAuth flow: could not save session state\",\n details: { error },\n });\n return;\n }\n\n // Redirect to OAuth URL (full page redirect)\n window.location.href = url;\n }\n\n /**\n * Check if returning from OAuth redirect and handle result\n * Call this on page load to detect OAuth return\n *\n * @returns true if OAuth return was detected and handled\n */\n static checkOAuthReturn(\n onSuccess: (grants: Grant[]) => void,\n onError: (error: AlterError) => void,\n ): boolean {\n // Check if we have OAuth state saved\n const stateJson = sessionStorage.getItem(\"alter_oauth_state\");\n if (!stateJson) {\n return false;\n }\n\n try {\n const state = JSON.parse(stateJson);\n\n // Check if this is a valid OAuth return (within 5 minutes)\n const elapsed = Date.now() - state.timestamp;\n if (elapsed > 5 * 60 * 1000) {\n // Stale state, clean up\n sessionStorage.removeItem(\"alter_oauth_state\");\n return false;\n }\n\n // Check URL for OAuth callback parameters\n const urlParams = new URLSearchParams(window.location.search);\n const success = urlParams.get(\"alter_connect_success\");\n const error = urlParams.get(\"alter_connect_error\");\n\n if (success === \"true\") {\n // Validate required fields\n const grantId = urlParams.get(\"grant_id\");\n const provider = urlParams.get(\"provider\");\n const accountIdentifier = urlParams.get(\"account_identifier\");\n\n if (!grantId || !provider || !accountIdentifier) {\n cleanupRedirectState(state.returnUrl);\n onError({\n code: \"invalid_response\",\n message: \"OAuth redirect returned incomplete grant data\",\n });\n return true;\n }\n\n const grant: Grant = {\n grant_id: grantId,\n provider: provider,\n provider_name: urlParams.get(\"provider_name\") || provider,\n account_identifier: accountIdentifier,\n timestamp: urlParams.get(\"timestamp\") || new Date().toISOString(),\n operation: parseOperation(urlParams.get(\"operation\")),\n scopes: urlParams.get(\"scopes\")?.split(\",\") || [],\n status: parseStatus(urlParams.get(\"status\")),\n };\n\n cleanupRedirectState(state.returnUrl);\n onSuccess([grant]); // Wrap in array for unified interface\n return true;\n }\n\n if (error) {\n const alterError: AlterError = {\n code: urlParams.get(\"error_code\") || \"oauth_error\",\n message:\n urlParams.get(\"error_description\") || \"OAuth authorization failed\",\n };\n\n cleanupRedirectState(state.returnUrl);\n onError(alterError);\n return true;\n }\n\n return false;\n } catch (err) {\n console.error(\"[OAuth Handler] Failed to check OAuth return:\", err);\n sessionStorage.removeItem(\"alter_oauth_state\");\n return false;\n }\n }\n\n /**\n * Open OAuth popup (for desktop)\n */\n openPopup(url: string): void {\n // Calculate popup position (centered)\n const left =\n window.screenX + (window.outerWidth - this.options.popupWidth) / 2;\n const top =\n window.screenY + (window.outerHeight - this.options.popupHeight) / 2;\n\n const features = [\n `width=${this.options.popupWidth}`,\n `height=${this.options.popupHeight}`,\n `left=${left}`,\n `top=${top}`,\n \"resizable=yes\",\n \"scrollbars=yes\",\n \"status=yes\",\n ].join(\",\");\n\n this.log(\"Opening OAuth popup:\", url);\n\n // Open popup\n this.popup = window.open(url, \"alter_oauth_popup\", features);\n\n if (!this.popup) {\n this.options.onError({\n code: \"popup_blocked\",\n message:\n \"Popup was blocked by browser. Please allow popups for this site.\",\n });\n return;\n }\n\n // Start monitoring popup\n this.startPolling();\n this.setupMessageListener();\n }\n\n /**\n * Close popup and clean up\n */\n close(): void {\n this.log(\"Closing OAuth handler\");\n\n // Close popup\n if (this.popup && !this.popup.closed) {\n this.popup.close();\n }\n this.popup = null;\n\n // Stop polling\n if (this.pollInterval !== null) {\n clearInterval(this.pollInterval);\n this.pollInterval = null;\n }\n\n // Remove message listener\n if (this.messageListener) {\n window.removeEventListener(\"message\", this.messageListener);\n this.messageListener = null;\n }\n }\n\n /**\n * Poll to detect when popup is closed\n */\n private startPolling(): void {\n this.pollInterval = window.setInterval(() => {\n if (this.settled) return;\n if (!this.popup || this.popup.closed) {\n this.log(\"Popup closed by user\");\n this.settled = true;\n this.close();\n this.options.onCancel();\n }\n }, 500);\n }\n\n /**\n * Listen for postMessage from OAuth callback page\n */\n private setupMessageListener(): void {\n this.messageListener = (event: MessageEvent) => {\n // Ignore if already settled (prevents race with polling)\n if (this.settled) return;\n\n // Validate origin against the expected backend origin\n if (event.origin !== this.expectedOrigin) {\n this.log(\n \"Rejected message from unexpected origin:\",\n event.origin,\n \"(expected:\",\n this.expectedOrigin + \")\",\n );\n return;\n }\n\n this.log(\"Received message from\", event.origin);\n\n const data = event.data;\n\n // Check message type\n if (data && typeof data === \"object\") {\n if (data.type === \"alter_connect_success\") {\n this.log(\"OAuth success\");\n\n // Accept both grants array and legacy connections array\n const rawGrants = Array.isArray(data.grants) ? data.grants : null;\n if (rawGrants) {\n this.log(\"Multi-provider success:\", rawGrants.length, \"grants\");\n const grants: Grant[] = [];\n for (const item of rawGrants) {\n const grantId = item.grant_id as string | undefined;\n if (!grantId || !item.provider) {\n this.log(\"Skipping invalid grant item:\", item);\n continue;\n }\n grants.push({\n grant_id: grantId,\n provider: item.provider as string,\n provider_name:\n (item.provider_name as string) || (item.provider as string),\n account_identifier: (item.account_identifier as string) || \"\",\n timestamp:\n (item.timestamp as string) || new Date().toISOString(),\n operation: parseOperation(item.operation as string | undefined),\n scopes: Array.isArray(item.scopes)\n ? (item.scopes as string[])\n : [],\n status: parseStatus(item.status as string | undefined),\n metadata: item.metadata as Grant[\"metadata\"],\n });\n }\n\n if (grants.length === 0) {\n this.settled = true;\n this.close();\n this.options.onError({\n code: \"invalid_response\",\n message: \"Server returned empty grants array\",\n });\n return;\n }\n\n this.settled = true;\n this.close();\n this.options.onSuccess(grants);\n return;\n }\n\n // No valid grants array found\n this.settled = true;\n this.close();\n this.options.onError({\n code: \"invalid_response\",\n message: \"Server returned success without grants array\",\n });\n } else if (data.type === \"alter_connect_error\") {\n this.log(\"OAuth error\");\n\n const error: AlterError = {\n code: (data.error as string) || \"oauth_error\",\n message:\n (data.error_description as string) ||\n \"OAuth authorization failed\",\n details: data as Record<string, unknown>,\n };\n\n this.settled = true;\n this.close();\n this.options.onError(error);\n }\n }\n };\n\n window.addEventListener(\"message\", this.messageListener);\n }\n\n /**\n * Log debug message\n */\n private log(...args: unknown[]): void {\n if (this.options.debug) {\n // Deliberate console.log: this IS the debug logger, and it only fires\n // when the integrator explicitly opts in via ``debug``.\n // eslint-disable-next-line no-console\n console.log(\"[OAuth Handler]\", ...args);\n }\n }\n}\n","/**\n * Main AlterConnect SDK class\n *\n * Opens the backend-served Connect UI in a popup. The backend handles\n * provider selection, branding, and OAuth — then sends the result back via postMessage.\n */\n\nimport type {\n AlterConnectConfig,\n OpenOptions,\n Grant,\n AlterError,\n} from \"../types\";\nimport {\n validateConfig,\n mergeConfig,\n debugLog,\n DEFAULT_ALTER_HOST,\n type FullConfig,\n} from \"./config\";\nimport { EventEmitter, type EventHandler } from \"./events\";\nimport { StateManager } from \"../state/manager\";\nimport { OAuthHandler } from \"../oauth/handler\";\n\n/**\n * SDK version\n */\nconst VERSION = \"0.2.0\";\n\n/**\n * Module-scoped flag so the non-default-host warning fires at most once\n * per process. Reset is intentionally not exposed — the warning exists to\n * surface accidental misconfigurations during dev, not to spam logs.\n */\nlet hasWarnedNonDefaultHost = false;\n\n/**\n * Main SDK class\n */\nexport class AlterConnect {\n private config: FullConfig;\n private eventEmitter: EventEmitter;\n private stateManager: StateManager;\n private _isInitialized: boolean;\n private _oauthHandler: OAuthHandler | null = null;\n private readonly _baseURL: string;\n private _perOpenCleanups: Array<() => void> = [];\n\n /**\n * Private constructor (use AlterConnect.create() instead)\n */\n private constructor(config: AlterConnectConfig = {}) {\n // Validate and merge config\n validateConfig(config);\n this.config = mergeConfig(config);\n\n // Resolve the Alter host once at construction time. `baseURL` is an\n // environment-level setting (production vs self-hosted vs local-dev),\n // not a per-`open()` decision — that's why it lives on the constructor\n // config and not on OpenOptions.\n this._baseURL = (config.baseURL ?? DEFAULT_ALTER_HOST).replace(/\\/+$/, \"\");\n if (this._baseURL !== DEFAULT_ALTER_HOST && !hasWarnedNonDefaultHost) {\n hasWarnedNonDefaultHost = true;\n // Log only the origin, not the full URL. validateConfig rejects\n // userinfo/search/hash, so the path is the only \"extra\" piece the\n // origin strips — but it costs us nothing to be defensive here in\n // case validateConfig's allowlist is ever broadened. The warn\n // message is forwarded to customer-side error trackers (Sentry,\n // Datadog RUM), so minimizing what flows through it is prudent.\n const origin = new URL(this._baseURL).origin;\n console.warn(\n `[alter-connect] Using non-default Alter host: ${origin}. ` +\n \"Unset baseURL in AlterConnect.create() for production.\",\n );\n }\n\n // Initialize components\n this.eventEmitter = new EventEmitter();\n this.stateManager = new StateManager();\n this._isInitialized = true;\n\n debugLog(this.config, \"Alter Connect SDK initialized\", {\n version: VERSION,\n baseURL: this._baseURL,\n });\n\n // Check if returning from OAuth redirect (mobile flow)\n this.checkRedirectReturn();\n }\n\n /**\n * Check if returning from OAuth redirect and handle result\n * This runs automatically on SDK initialization\n */\n private checkRedirectReturn(): void {\n const handled = OAuthHandler.checkOAuthReturn(\n (grants: Grant[]) => {\n debugLog(this.config, \"OAuth redirect return - success:\", grants);\n // Emit success event (will be picked up by event handlers if SDK was re-initialized)\n this.eventEmitter.emit(\"success\", grants);\n },\n (error: AlterError) => {\n debugLog(this.config, \"OAuth redirect return - error:\", error);\n // Emit error event\n this.eventEmitter.emit(\"error\", error);\n },\n );\n\n if (handled) {\n debugLog(this.config, \"OAuth redirect return detected and handled\");\n }\n }\n\n /**\n * Create a new AlterConnect instance\n *\n * @param config Optional configuration\n * @returns AlterConnect instance\n *\n * @example\n * const alterConnect = AlterConnect.create();\n * const alterConnect = AlterConnect.create({ debug: true });\n */\n static create(config?: AlterConnectConfig): AlterConnect {\n return new AlterConnect(config);\n }\n\n /**\n * Open the Connect UI in a popup window\n *\n * Opens the backend-served Connect UI at {baseURL}/oauth/connect#session={token}.\n * The backend handles provider selection, branding, and OAuth flow.\n * On success, the backend sends a postMessage back to this window.\n *\n * @param options Configuration including the session token from your backend\n *\n * @example\n * // 1. Get session token from YOUR backend\n * const { session_token } = await fetch('/api/alter/create-session').then(r => r.json());\n *\n * // 2. Open Connect UI\n * await alterConnect.open({\n * token: session_token,\n * onSuccess: (grants) => {\n * console.log('Connected!', grants);\n * },\n * onError: (error) => {\n * console.error('Failed:', error);\n * }\n * });\n */\n async open(options: OpenOptions): Promise<void> {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call open() - SDK instance has been destroyed\",\n );\n }\n\n debugLog(this.config, \"Opening Connect UI\");\n\n // Validate required options\n if (!options.token || typeof options.token !== \"string\") {\n throw this.createError(\n \"invalid_options\",\n \"Session token is required. Create one from your backend using POST /sdk/oauth/connect/session\",\n );\n }\n\n if (!options.onSuccess || typeof options.onSuccess !== \"function\") {\n throw this.createError(\n \"invalid_options\",\n \"onSuccess callback is required\",\n );\n }\n\n // Check if already open\n if (this.isOpen()) {\n debugLog(this.config, \"Connect UI is already open\");\n return;\n }\n\n // Create OAuth handler first — this validates baseURL and throws on invalid input.\n // We do this BEFORE setting isOpen to avoid leaving the SDK in a broken state.\n this._oauthHandler = new OAuthHandler({\n baseURL: this._baseURL,\n onSuccess: (grants: Grant[]) => {\n debugLog(this.config, \"OAuth success:\", grants);\n this.handleOAuthSuccess(grants);\n },\n onError: (error: AlterError) => {\n debugLog(this.config, \"OAuth error:\", error);\n this.handleOAuthError(error);\n },\n onCancel: () => {\n debugLog(this.config, \"OAuth cancelled (popup closed)\");\n this.handleOAuthCancel();\n },\n popupWidth: 500,\n popupHeight: 700,\n debug: this.config.debug,\n });\n\n // Update state (after handler creation succeeds)\n this.stateManager.setState({\n isOpen: true,\n error: null,\n sessionToken: options.token,\n });\n\n // Register event handlers\n this.registerEventHandlers(options);\n\n // Build the Connect UI URL — the backend serves the full UI\n const connectURL = `${this._baseURL}/sdk/oauth/connect#session=${encodeURIComponent(options.token)}`;\n\n debugLog(this.config, \"Connect URL:\", connectURL);\n\n // Open the Connect UI (popup or redirect depending on device)\n this._oauthHandler.startOAuth(connectURL);\n\n // Emit analytics event\n if (options.onEvent) {\n options.onEvent(\"connect_opened\", {\n timestamp: new Date().toISOString(),\n });\n }\n\n debugLog(this.config, \"Connect UI opened successfully\");\n }\n\n /**\n * Close the Connect UI\n */\n close(): void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call close() - SDK instance has been destroyed\",\n );\n }\n\n debugLog(this.config, \"Closing Connect UI\");\n\n // Clean up OAuth handler if active\n if (this._oauthHandler) {\n this._oauthHandler.close();\n this._oauthHandler = null;\n }\n\n // Remove only per-open handlers (preserves global handlers from on())\n this._perOpenCleanups.forEach((cleanup) => cleanup());\n this._perOpenCleanups = [];\n\n // Update state\n this.stateManager.setState({ isOpen: false });\n\n this.eventEmitter.emit(\"close\");\n }\n\n /**\n * Destroy the SDK instance and clean up resources\n */\n destroy(): void {\n if (!this._isInitialized) {\n // Already destroyed, silently return\n return;\n }\n\n debugLog(this.config, \"Destroying SDK instance\");\n\n // Clean up OAuth handler if active\n if (this._oauthHandler) {\n this._oauthHandler.close();\n this._oauthHandler = null;\n }\n\n // Update state\n if (this.stateManager.get(\"isOpen\")) {\n this.stateManager.setState({ isOpen: false });\n }\n\n // Clean up resources\n this.eventEmitter.removeAllListeners();\n this.stateManager.clearListeners();\n this._isInitialized = false;\n }\n\n /**\n * Register an event listener\n */\n on(event: string, handler: EventHandler): () => void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call on() - SDK instance has been destroyed\",\n );\n }\n\n return this.eventEmitter.on(event, handler);\n }\n\n /**\n * Remove an event listener\n */\n off(event: string, handler: EventHandler): void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call off() - SDK instance has been destroyed\",\n );\n }\n\n this.eventEmitter.off(event, handler);\n }\n\n /**\n * Check if Connect UI is open\n */\n isOpen(): boolean {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call isOpen() - SDK instance has been destroyed\",\n );\n }\n\n return this.stateManager.get(\"isOpen\");\n }\n\n /**\n * Get SDK version\n */\n getVersion(): string {\n return VERSION;\n }\n\n /**\n * Clean up OAuth handler reference after a flow completes.\n * The OAuthHandler.close() is already called internally by the handler\n * before invoking callbacks, so we only need to null the reference here.\n */\n private cleanupHandler(): void {\n this._oauthHandler = null;\n }\n\n /**\n * Handle OAuth success\n */\n private handleOAuthSuccess(grants: Grant[]): void {\n this.cleanupHandler();\n this.eventEmitter.emit(\"success\", grants);\n }\n\n /**\n * Handle OAuth error\n */\n private handleOAuthError(error: AlterError): void {\n this.cleanupHandler();\n this.stateManager.setState({ error });\n this.eventEmitter.emit(\"error\", error);\n }\n\n /**\n * Handle OAuth cancellation (user closed popup)\n */\n private handleOAuthCancel(): void {\n this.cleanupHandler();\n this.eventEmitter.emit(\"exit\");\n }\n\n /**\n * Register event handlers from OpenOptions\n */\n private registerEventHandlers(options: OpenOptions): void {\n // Success handler\n if (options.onSuccess) {\n this._perOpenCleanups.push(\n this.eventEmitter.on(\"success\", (grants: Grant[]) => {\n options.onSuccess(grants);\n this.close();\n }),\n );\n }\n\n // Exit handler\n if (options.onExit) {\n this._perOpenCleanups.push(\n this.eventEmitter.on(\"exit\", () => {\n options.onExit!();\n this.close();\n }),\n );\n }\n\n // Error handler — always registered to ensure close() is called\n this._perOpenCleanups.push(\n this.eventEmitter.on(\"error\", (error: AlterError) => {\n if (options.onError) {\n options.onError(error);\n }\n this.close();\n }),\n );\n\n // Event handler (analytics)\n if (options.onEvent) {\n this._perOpenCleanups.push(\n this.eventEmitter.on(\n \"event\",\n (eventName: string, metadata: Record<string, unknown>) => {\n options.onEvent!(eventName, metadata);\n },\n ),\n );\n }\n }\n\n /**\n * Create an AlterError object\n */\n private createError(\n code: string,\n message: string,\n details?: Record<string, unknown>,\n ): Error {\n const error = new Error(message) as Error & {\n code?: string;\n details?: Record<string, unknown>;\n };\n error.code = code;\n error.details = details;\n return error;\n }\n}\n"],"names":["DEFAULT_ALTER_HOST","debugLog","config","args","debug","console","log","EventEmitter","constructor","this","events","Map","on","event","handler","has","set","Set","get","add","off","handlers","delete","emit","forEach","error","removeAllListeners","clear","StateManager","state","isOpen","sessionToken","listeners","getState","key","setState","updates","notifyListeners","subscribe","listener","clearListeners","currentState","isMobileDevice","userAgent","navigator","vendor","window","opera","isMobileUA","test","hasTouchScreen","maxTouchPoints","msMaxTouchPoints","isSmallScreen","innerWidth","getDeviceType","parseOperation","value","parseStatus","cleanupRedirectState","returnUrl","sessionStorage","removeItem","cleanUrl","URL","history","replaceState","document","title","pathname","search","hash","location","OAuthHandler","options","popup","pollInterval","messageListener","settled","baseURL","onSuccess","onError","onCancel","popupWidth","popupHeight","expectedOrigin","origin","Error","startOAuth","url","parsed","code","message","deviceType","innerHeight","shouldUseRedirectFlow","openPopup","startRedirectFlow","timestamp","Date","now","href","setItem","JSON","stringify","details","checkOAuthReturn","stateJson","getItem","parse","urlParams","URLSearchParams","success","grantId","provider","accountIdentifier","grant","grant_id","provider_name","account_identifier","toISOString","operation","scopes","split","status","alterError","err","left","screenX","outerWidth","top","screenY","outerHeight","features","join","open","startPolling","setupMessageListener","close","closed","clearInterval","removeEventListener","setInterval","data","type","rawGrants","Array","isArray","grants","length","item","push","metadata","error_description","addEventListener","VERSION","hasWarnedNonDefaultHost","AlterConnect","_oauthHandler","_perOpenCleanups","undefined","validateConfig","mergeConfig","_baseURL","replace","warn","eventEmitter","stateManager","_isInitialized","version","checkRedirectReturn","create","createError","token","handleOAuthSuccess","handleOAuthError","handleOAuthCancel","registerEventHandlers","connectURL","encodeURIComponent","onEvent","cleanup","destroy","getVersion","cleanupHandler","onExit","eventName"],"mappings":"AAcO,MAAMA,EAAqB,yCAwHlBC,EAASC,KAAuBC,GAC1CD,EAAOE,OAITC,QAAQC,IAAI,qBAAsBH,EAEtC,OChIaI,EAGX,WAAAC,GACEC,KAAKC,OAAS,IAAIC,GACpB,CAMA,EAAAC,CAAGC,EAAeC,GAQhB,OAPKL,KAAKC,OAAOK,IAAIF,IACnBJ,KAAKC,OAAOM,IAAIH,EAAO,IAAII,KAG7BR,KAAKC,OAAOQ,IAAIL,GAAQM,IAAIL,GAGrB,IAAML,KAAKW,IAAIP,EAAOC,EAC/B,CAKA,GAAAM,CAAIP,EAAeC,GACjB,MAAMO,EAAWZ,KAAKC,OAAOQ,IAAIL,GAC7BQ,GACFA,EAASC,OAAOR,EAEpB,CAKA,IAAAS,CAAKV,KAAkBV,GACrB,MAAMkB,EAAWZ,KAAKC,OAAOQ,IAAIL,GAC7BQ,GACFA,EAASG,QAASV,IAChB,IACGA,KAA+CX,EAClD,CAAE,MAAOsB,GACPpB,QAAQoB,MACN,+CAA+CZ,MAC/CY,EAEJ,GAGN,CAKA,kBAAAC,CAAmBb,GACbA,EACFJ,KAAKC,OAAOY,OAAOT,GAEnBJ,KAAKC,OAAOiB,OAEhB,QCrDWC,EAIX,WAAApB,GACEC,KAAKoB,MAAQ,CACXC,QAAQ,EACRC,aAAc,KACdN,MAAO,MAEThB,KAAKuB,UAAY,IAAIf,GACvB,CAKA,QAAAgB,GACE,MAAO,IAAKxB,KAAKoB,MACnB,CAKA,GAAAX,CAA8BgB,GAC5B,OAAOzB,KAAKoB,MAAMK,EACpB,CAKA,QAAAC,CAASC,GACP3B,KAAKoB,MAAQ,IAAKpB,KAAKoB,SAAUO,GACjC3B,KAAK4B,iBACP,CAKA,SAAAC,CAAUC,GAIR,OAHA9B,KAAKuB,UAAUb,IAAIoB,GAGZ,KACL9B,KAAKuB,UAAUV,OAAOiB,GAE1B,CAKA,cAAAC,GACE/B,KAAKuB,UAAUL,OACjB,CAKQ,eAAAU,GACN,MAAMI,EAAehC,KAAKwB,WAC1BxB,KAAKuB,UAAUR,QAASe,IACtB,IACEA,EAASE,EACX,CAAE,MAAOhB,GACPpB,QAAQoB,MAAM,2CAA4CA,EAC5D,GAEJ,WC/EciB,IAId,MAAMC,EACJC,UAAUD,WACVC,UAAUC,QACTC,OAAuCC,OACxC,GAKIC,EADJ,iEAC6BC,KAAKN,GAG9BO,EACJ,iBAAkBJ,QAClBF,UAAUO,eAAiB,IACzBP,UACCQ,kBAAoB,GAAK,EAGxBC,EAAgBP,OAAOQ,YAAc,IAE3C,OAAON,GAAeE,GAAkBG,CAC1C,UAqBgBE,IACd,OAhBOb,KAAoBI,OAAOQ,YAAc,IAgBpB,QAR1BZ,KAAoBI,OAAOQ,WAAa,KAAOR,OAAOQ,YAAc,KASzC,SACtB,SACT,CCnCA,SAASE,EAAeC,GACtB,MAAc,WAAVA,EAA2B,SACxB,UACT,CAKA,SAASC,EAAYD,GACnB,MAAc,YAAVA,EAA4B,UAClB,UAAVA,EAA0B,QACvB,QACT,CAKA,SAASE,EAAqBC,GAC5BC,eAAeC,WAAW,qBAC1B,IACE,MAAMC,EAAW,IAAIC,IAAIJ,GACzBd,OAAOmB,QAAQC,aACb,CAAA,EACAC,SAASC,MACTL,EAASM,SAAWN,EAASO,OAASP,EAASQ,KAEnD,CAAE,MAEAzB,OAAOmB,QAAQC,aAAa,CAAA,EAAIC,SAASC,MAAOtB,OAAO0B,SAASH,SAClE,CACF,OAKaI,EAQX,WAAAjE,CAAYkE,GAPJjE,KAAAkE,MAAuB,KACvBlE,KAAAmE,aAA8B,KAC9BnE,KAAAoE,gBAA0D,KAG1DpE,KAAAqE,SAAmB,EAGzBrE,KAAKiE,QAAU,CACbK,QAASL,EAAQK,QACjBC,UAAWN,EAAQM,UACnBC,QAASP,EAAQO,QACjBC,SAAUR,EAAQQ,SAClBC,WAAYT,EAAQS,YAAc,IAClCC,YAAaV,EAAQU,aAAe,IACpChF,MAAOsE,EAAQtE,QAAS,EACxBiF,eAAgBX,EAAQW,gBAAkB,IAI5C,IACE5E,KAAK4E,eAAiB,IAAIrB,IAAIU,EAAQK,SAASO,MACjD,CAAE,MACA,MAAM,IAAIC,MACR,qBAAqBb,EAAQK,sDAAsD/E,OAEvF,CACF,CAKA,UAAAwF,CAAWC,GAGT,IAAKhF,KAAKiE,QAAQW,eAChB,IACE,MAAMK,EAAS,IAAI1B,IAAIyB,GACvBhF,KAAKiE,QAAQW,eAAiBK,EAAOJ,OACrC7E,KAAKH,IAAI,2BAA4BG,KAAKiE,QAAQW,eACpD,CAAE,MAOA,OANA5E,KAAKH,IAAI,mDAAoDmF,QAC7DhF,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,oBACNC,QACE,uEAGN,aDtCJ,MAAMC,EAAatC,IAGnB,MAAmB,UAAfsC,GAGe,WAAfA,GAA2B/C,OAAOgD,YAAchD,OAAOQ,UAM7D,CC6BQyC,IAIFtF,KAAKH,IAAI,gCACTG,KAAKuF,UAAUP,KAJfhF,KAAKH,IAAI,yCACTG,KAAKwF,kBAAkBR,GAK3B,CAMQ,iBAAAQ,CAAkBR,GACxBhF,KAAKH,IAAI,0BAA2BmF,GAGpC,MAAM5D,EAAQ,CACZqE,UAAWC,KAAKC,MAChBxC,UAAWd,OAAO0B,SAAS6B,MAG7B,IACExC,eAAeyC,QAAQ,oBAAqBC,KAAKC,UAAU3E,GAC7D,CAAE,MAAOJ,GAOP,OANAhB,KAAKH,IAAI,wBAAyBmB,QAClChB,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,iBACNC,QAAS,2DACTa,QAAS,CAAEhF,UAGf,CAGAqB,OAAO0B,SAAS6B,KAAOZ,CACzB,CAQA,uBAAOiB,CACL1B,EACAC,GAGA,MAAM0B,EAAY9C,eAAe+C,QAAQ,qBACzC,IAAKD,EACH,OAAO,EAGT,IACE,MAAM9E,EAAQ0E,KAAKM,MAAMF,GAIzB,GADgBR,KAAKC,MAAQvE,EAAMqE,UACrB,IAGZ,OADArC,eAAeC,WAAW,sBACnB,EAIT,MAAMgD,EAAY,IAAIC,gBAAgBjE,OAAO0B,SAASF,QAChD0C,EAAUF,EAAU5F,IAAI,yBACxBO,EAAQqF,EAAU5F,IAAI,uBAE5B,GAAgB,SAAZ8F,EAAoB,CAEtB,MAAMC,EAAUH,EAAU5F,IAAI,YACxBgG,EAAWJ,EAAU5F,IAAI,YACzBiG,EAAoBL,EAAU5F,IAAI,sBAExC,IAAK+F,IAAYC,IAAaC,EAM5B,OALAxD,EAAqB9B,EAAM+B,WAC3BqB,EAAQ,CACNU,KAAM,mBACNC,QAAS,mDAEJ,EAGT,MAAMwB,EAAe,CACnBC,SAAUJ,EACVC,SAAUA,EACVI,cAAeR,EAAU5F,IAAI,kBAAoBgG,EACjDK,mBAAoBJ,EACpBjB,UAAWY,EAAU5F,IAAI,eAAgB,IAAIiF,MAAOqB,cACpDC,UAAWjE,EAAesD,EAAU5F,IAAI,cACxCwG,OAAQZ,EAAU5F,IAAI,WAAWyG,MAAM,MAAQ,GAC/CC,OAAQlE,EAAYoD,EAAU5F,IAAI,YAKpC,OAFAyC,EAAqB9B,EAAM+B,WAC3BoB,EAAU,CAACoC,KACJ,CACT,CAEA,GAAI3F,EAAO,CACT,MAAMoG,EAAyB,CAC7BlC,KAAMmB,EAAU5F,IAAI,eAAiB,cACrC0E,QACEkB,EAAU5F,IAAI,sBAAwB,8BAK1C,OAFAyC,EAAqB9B,EAAM+B,WAC3BqB,EAAQ4C,IACD,CACT,CAEA,OAAO,CACT,CAAE,MAAOC,GAGP,OAFAzH,QAAQoB,MAAM,gDAAiDqG,GAC/DjE,eAAeC,WAAW,sBACnB,CACT,CACF,CAKA,SAAAkC,CAAUP,GAER,MAAMsC,EACJjF,OAAOkF,SAAWlF,OAAOmF,WAAaxH,KAAKiE,QAAQS,YAAc,EAC7D+C,EACJpF,OAAOqF,SAAWrF,OAAOsF,YAAc3H,KAAKiE,QAAQU,aAAe,EAE/DiD,EAAW,CACf,SAAS5H,KAAKiE,QAAQS,aACtB,UAAU1E,KAAKiE,QAAQU,cACvB,QAAQ2C,IACR,OAAOG,IACP,gBACA,iBACA,cACAI,KAAK,KAEP7H,KAAKH,IAAI,uBAAwBmF,GAGjChF,KAAKkE,MAAQ7B,OAAOyF,KAAK9C,EAAK,oBAAqB4C,GAE9C5H,KAAKkE,OAUVlE,KAAK+H,eACL/H,KAAKgI,wBAVHhI,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,gBACNC,QACE,oEAQR,CAKA,KAAA8C,GACEjI,KAAKH,IAAI,yBAGLG,KAAKkE,QAAUlE,KAAKkE,MAAMgE,QAC5BlI,KAAKkE,MAAM+D,QAEbjI,KAAKkE,MAAQ,KAGa,OAAtBlE,KAAKmE,eACPgE,cAAcnI,KAAKmE,cACnBnE,KAAKmE,aAAe,MAIlBnE,KAAKoE,kBACP/B,OAAO+F,oBAAoB,UAAWpI,KAAKoE,iBAC3CpE,KAAKoE,gBAAkB,KAE3B,CAKQ,YAAA2D,GACN/H,KAAKmE,aAAe9B,OAAOgG,YAAY,KACjCrI,KAAKqE,SACJrE,KAAKkE,QAASlE,KAAKkE,MAAMgE,SAC5BlI,KAAKH,IAAI,wBACTG,KAAKqE,SAAU,EACfrE,KAAKiI,QACLjI,KAAKiE,QAAQQ,aAEd,IACL,CAKQ,oBAAAuD,GACNhI,KAAKoE,gBAAmBhE,IAEtB,GAAIJ,KAAKqE,QAAS,OAGlB,GAAIjE,EAAMyE,SAAW7E,KAAK4E,eAOxB,YANA5E,KAAKH,IACH,2CACAO,EAAMyE,OACN,aACA7E,KAAK4E,eAAiB,KAK1B5E,KAAKH,IAAI,wBAAyBO,EAAMyE,QAExC,MAAMyD,EAAOlI,EAAMkI,KAGnB,GAAIA,GAAwB,iBAATA,EACjB,GAAkB,0BAAdA,EAAKC,KAAkC,CACzCvI,KAAKH,IAAI,iBAGT,MAAM2I,EAAYC,MAAMC,QAAQJ,EAAKK,QAAUL,EAAKK,OAAS,KAC7D,GAAIH,EAAW,CACbxI,KAAKH,IAAI,0BAA2B2I,EAAUI,OAAQ,UACtD,MAAMD,EAAkB,GACxB,IAAK,MAAME,KAAQL,EAAW,CAC5B,MAAMhC,EAAUqC,EAAKjC,SAChBJ,GAAYqC,EAAKpC,SAItBkC,EAAOG,KAAK,CACVlC,SAAUJ,EACVC,SAAUoC,EAAKpC,SACfI,cACGgC,EAAKhC,eAA6BgC,EAAKpC,SAC1CK,mBAAqB+B,EAAK/B,oBAAiC,GAC3DrB,UACGoD,EAAKpD,YAAwB,IAAIC,MAAOqB,cAC3CC,UAAWjE,EAAe8F,EAAK7B,WAC/BC,OAAQwB,MAAMC,QAAQG,EAAK5B,QACtB4B,EAAK5B,OACN,GACJE,OAAQlE,EAAY4F,EAAK1B,QACzB4B,SAAUF,EAAKE,WAhBf/I,KAAKH,IAAI,+BAAgCgJ,EAkB7C,CAEA,OAAsB,IAAlBF,EAAOC,QACT5I,KAAKqE,SAAU,EACfrE,KAAKiI,aACLjI,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,mBACNC,QAAS,yCAKbnF,KAAKqE,SAAU,EACfrE,KAAKiI,aACLjI,KAAKiE,QAAQM,UAAUoE,GAEzB,CAGA3I,KAAKqE,SAAU,EACfrE,KAAKiI,QACLjI,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,mBACNC,QAAS,gDAEb,MAAO,GAAkB,wBAAdmD,EAAKC,KAAgC,CAC9CvI,KAAKH,IAAI,eAET,MAAMmB,EAAoB,CACxBkE,KAAOoD,EAAKtH,OAAoB,cAChCmE,QACGmD,EAAKU,mBACN,6BACFhD,QAASsC,GAGXtI,KAAKqE,SAAU,EACfrE,KAAKiI,QACLjI,KAAKiE,QAAQO,QAAQxD,EACvB,GAIJqB,OAAO4G,iBAAiB,UAAWjJ,KAAKoE,gBAC1C,CAKQ,GAAAvE,IAAOH,GACTM,KAAKiE,QAAQtE,OAIfC,QAAQC,IAAI,qBAAsBH,EAEtC,ECzYF,MAAMwJ,EAAU,QAOhB,IAAIC,GAA0B,QAKjBC,EAYX,WAAArJ,CAAoBN,EAA6B,IAU/C,GAjBMO,KAAAqJ,cAAqC,KAErCrJ,KAAAsJ,iBAAsC,GLX1C,SAAyB7J,GAC7B,QAAuB8J,IAAnB9J,EAAO6E,QAYT,MARwB7E,EAAO6E,QAQzB,IAAIQ,MACR,iHAuEN,CKnEI0E,CAAe/J,GACfO,KAAKP,OLuEH,SAAsBA,GAC1B,MAAO,CACLE,MAAOF,EAAOE,QAAS,EAE3B,CK3EkB8J,CAAYhK,GAM1BO,KAAK0J,UAAYjK,EAAO6E,SAAW/E,GAAoBoK,QAAQ,OAAQ,IACnE3J,KAAK0J,WAAanK,IAAuB4J,EAAyB,CACpEA,GAA0B,EAO1B,MAAMtE,EAAS,IAAItB,IAAIvD,KAAK0J,UAAU7E,OACtCjF,QAAQgK,KACN,iDAAiD/E,4DAGrD,CAGA7E,KAAK6J,aAAe,IAAI/J,EACxBE,KAAK8J,aAAe,IAAI3I,EACxBnB,KAAK+J,gBAAiB,EAEtBvK,EAASQ,KAAKP,OAAQ,gCAAiC,CACrDuK,QAASd,EACT5E,QAAStE,KAAK0J,WAIhB1J,KAAKiK,qBACP,CAMQ,mBAAAA,GACUjG,EAAaiC,iBAC1B0C,IACCnJ,EAASQ,KAAKP,OAAQ,mCAAoCkJ,GAE1D3I,KAAK6J,aAAa/I,KAAK,UAAW6H,IAEnC3H,IACCxB,EAASQ,KAAKP,OAAQ,iCAAkCuB,GAExDhB,KAAK6J,aAAa/I,KAAK,QAASE,MAKlCxB,EAASQ,KAAKP,OAAQ,6CAE1B,CAYA,aAAOyK,CAAOzK,GACZ,OAAO,IAAI2J,EAAa3J,EAC1B,CA0BA,UAAMqI,CAAK7D,GAET,IAAKjE,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,wDAOJ,GAHA3K,EAASQ,KAAKP,OAAQ,uBAGjBwE,EAAQmG,OAAkC,iBAAlBnG,EAAQmG,MACnC,MAAMpK,KAAKmK,YACT,kBACA,iGAIJ,IAAKlG,EAAQM,WAA0C,mBAAtBN,EAAQM,UACvC,MAAMvE,KAAKmK,YACT,kBACA,kCAKJ,GAAInK,KAAKqB,SAEP,YADA7B,EAASQ,KAAKP,OAAQ,8BAMxBO,KAAKqJ,cAAgB,IAAIrF,EAAa,CACpCM,QAAStE,KAAK0J,SACdnF,UAAYoE,IACVnJ,EAASQ,KAAKP,OAAQ,iBAAkBkJ,GACxC3I,KAAKqK,mBAAmB1B,IAE1BnE,QAAUxD,IACRxB,EAASQ,KAAKP,OAAQ,eAAgBuB,GACtChB,KAAKsK,iBAAiBtJ,IAExByD,SAAU,KACRjF,EAASQ,KAAKP,OAAQ,kCACtBO,KAAKuK,qBAEP7F,WAAY,IACZC,YAAa,IACbhF,MAAOK,KAAKP,OAAOE,QAIrBK,KAAK8J,aAAapI,SAAS,CACzBL,QAAQ,EACRL,MAAO,KACPM,aAAc2C,EAAQmG,QAIxBpK,KAAKwK,sBAAsBvG,GAG3B,MAAMwG,EAAa,GAAGzK,KAAK0J,sCAAsCgB,mBAAmBzG,EAAQmG,SAE5F5K,EAASQ,KAAKP,OAAQ,eAAgBgL,GAGtCzK,KAAKqJ,cAActE,WAAW0F,GAG1BxG,EAAQ0G,SACV1G,EAAQ0G,QAAQ,iBAAkB,CAChClF,WAAW,IAAIC,MAAOqB,gBAI1BvH,EAASQ,KAAKP,OAAQ,iCACxB,CAKA,KAAAwI,GAEE,IAAKjI,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,yDAIJ3K,EAASQ,KAAKP,OAAQ,sBAGlBO,KAAKqJ,gBACPrJ,KAAKqJ,cAAcpB,QACnBjI,KAAKqJ,cAAgB,MAIvBrJ,KAAKsJ,iBAAiBvI,QAAS6J,GAAYA,KAC3C5K,KAAKsJ,iBAAmB,GAGxBtJ,KAAK8J,aAAapI,SAAS,CAAEL,QAAQ,IAErCrB,KAAK6J,aAAa/I,KAAK,QACzB,CAKA,OAAA+J,GACO7K,KAAK+J,iBAKVvK,EAASQ,KAAKP,OAAQ,2BAGlBO,KAAKqJ,gBACPrJ,KAAKqJ,cAAcpB,QACnBjI,KAAKqJ,cAAgB,MAInBrJ,KAAK8J,aAAarJ,IAAI,WACxBT,KAAK8J,aAAapI,SAAS,CAAEL,QAAQ,IAIvCrB,KAAK6J,aAAa5I,qBAClBjB,KAAK8J,aAAa/H,iBAClB/B,KAAK+J,gBAAiB,EACxB,CAKA,EAAA5J,CAAGC,EAAeC,GAEhB,IAAKL,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,sDAIJ,OAAOnK,KAAK6J,aAAa1J,GAAGC,EAAOC,EACrC,CAKA,GAAAM,CAAIP,EAAeC,GAEjB,IAAKL,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,uDAIJnK,KAAK6J,aAAalJ,IAAIP,EAAOC,EAC/B,CAKA,MAAAgB,GAEE,IAAKrB,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,0DAIJ,OAAOnK,KAAK8J,aAAarJ,IAAI,SAC/B,CAKA,UAAAqK,GACE,OAAO5B,CACT,CAOQ,cAAA6B,GACN/K,KAAKqJ,cAAgB,IACvB,CAKQ,kBAAAgB,CAAmB1B,GACzB3I,KAAK+K,iBACL/K,KAAK6J,aAAa/I,KAAK,UAAW6H,EACpC,CAKQ,gBAAA2B,CAAiBtJ,GACvBhB,KAAK+K,iBACL/K,KAAK8J,aAAapI,SAAS,CAAEV,UAC7BhB,KAAK6J,aAAa/I,KAAK,QAASE,EAClC,CAKQ,iBAAAuJ,GACNvK,KAAK+K,iBACL/K,KAAK6J,aAAa/I,KAAK,OACzB,CAKQ,qBAAA0J,CAAsBvG,GAExBA,EAAQM,WACVvE,KAAKsJ,iBAAiBR,KACpB9I,KAAK6J,aAAa1J,GAAG,UAAYwI,IAC/B1E,EAAQM,UAAUoE,GAClB3I,KAAKiI,WAMPhE,EAAQ+G,QACVhL,KAAKsJ,iBAAiBR,KACpB9I,KAAK6J,aAAa1J,GAAG,OAAQ,KAC3B8D,EAAQ+G,SACRhL,KAAKiI,WAMXjI,KAAKsJ,iBAAiBR,KACpB9I,KAAK6J,aAAa1J,GAAG,QAAUa,IACzBiD,EAAQO,SACVP,EAAQO,QAAQxD,GAElBhB,KAAKiI,WAKLhE,EAAQ0G,SACV3K,KAAKsJ,iBAAiBR,KACpB9I,KAAK6J,aAAa1J,GAChB,QACA,CAAC8K,EAAmBlC,KAClB9E,EAAQ0G,QAASM,EAAWlC,KAKtC,CAKQ,WAAAoB,CACNjF,EACAC,EACAa,GAEA,MAAMhF,EAAQ,IAAI8D,MAAMK,GAMxB,OAFAnE,EAAMkE,KAAOA,EACblE,EAAMgF,QAAUA,EACThF,CACT"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).AlterConnect=t()}(this,function(){"use strict";const e="https://backend.alterauth.com";function t(e,...t){e.debug&&console.log("[Alter Connect]",...t)}class s{constructor(){this.events=new Map}on(e,t){return this.events.has(e)||this.events.set(e,new Set),this.events.get(e).add(t),()=>this.off(e,t)}off(e,t){const s=this.events.get(e);s&&s.delete(t)}emit(e,...t){const s=this.events.get(e);s&&s.forEach(s=>{try{s(...t)}catch(t){console.error(`[Alter Connect] Error in event handler for '${e}':`,t)}})}removeAllListeners(e){e?this.events.delete(e):this.events.clear()}}class n{constructor(){this.state={isOpen:!1,sessionToken:null,error:null},this.listeners=new Set}getState(){return{...this.state}}get(e){return this.state[e]}setState(e){this.state={...this.state,...e},this.notifyListeners()}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}clearListeners(){this.listeners.clear()}notifyListeners(){const e=this.getState();this.listeners.forEach(t=>{try{t(e)}catch(e){console.error("[Alter Connect] Error in state listener:",e)}})}}function i(){const e=navigator.userAgent||navigator.vendor||window.opera,t=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(e),s="ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0,n=window.innerWidth<=768;return t||s&&n}function r(){return i()&&window.innerWidth<=480?"phone":i()&&window.innerWidth>480&&window.innerWidth<=1024?"tablet":"desktop"}function o(e){return"reauth"===e?"reauth":"creation"}function a(e){return"pending"===e?"pending":"error"===e?"error":"active"}function h(e){sessionStorage.removeItem("alter_oauth_state");try{const t=new URL(e);window.history.replaceState({},document.title,t.pathname+t.search+t.hash)}catch{window.history.replaceState({},document.title,window.location.pathname)}}class c{constructor(t){this.popup=null,this.pollInterval=null,this.messageListener=null,this.settled=!1,this.options={baseURL:t.baseURL,onSuccess:t.onSuccess,onError:t.onError,onCancel:t.onCancel,popupWidth:t.popupWidth||500,popupHeight:t.popupHeight||700,debug:t.debug||!1,expectedOrigin:t.expectedOrigin||""};try{this.expectedOrigin=new URL(t.baseURL).origin}catch{throw new Error(`Invalid baseURL: "${t.baseURL}". Must be a full URL with protocol (e.g., "${e}").`)}}startOAuth(e){if(!this.options.expectedOrigin)try{const t=new URL(e);this.options.expectedOrigin=t.origin,this.log("Derived expected origin:",this.options.expectedOrigin)}catch{return this.log("Failed to parse OAuth URL for origin validation:",e),void this.options.onError({code:"invalid_oauth_url",message:"Failed to determine origin from OAuth URL. Cannot proceed securely."})}!function(){const e=r();return"phone"===e||"tablet"===e&&window.innerHeight>window.innerWidth}()?(this.log("Using popup flow for desktop"),this.openPopup(e)):(this.log("Using redirect flow for mobile device"),this.startRedirectFlow(e))}startRedirectFlow(e){this.log("Starting redirect flow:",e);const t={timestamp:Date.now(),returnUrl:window.location.href};try{sessionStorage.setItem("alter_oauth_state",JSON.stringify(t))}catch(e){return this.log("Failed to save state:",e),void this.options.onError({code:"redirect_error",message:"Failed to start OAuth flow: could not save session state",details:{error:e}})}window.location.href=e}static checkOAuthReturn(e,t){const s=sessionStorage.getItem("alter_oauth_state");if(!s)return!1;try{const n=JSON.parse(s);if(Date.now()-n.timestamp>3e5)return sessionStorage.removeItem("alter_oauth_state"),!1;const i=new URLSearchParams(window.location.search),r=i.get("alter_connect_success"),c=i.get("alter_connect_error");if("true"===r){const s=i.get("grant_id"),r=i.get("provider"),c=i.get("account_identifier");if(!s||!r||!c)return h(n.returnUrl),t({code:"invalid_response",message:"OAuth redirect returned incomplete grant data"}),!0;const l={grant_id:s,provider:r,provider_name:i.get("provider_name")||r,account_identifier:c,timestamp:i.get("timestamp")||(new Date).toISOString(),operation:o(i.get("operation")),scopes:i.get("scopes")?.split(",")||[],status:a(i.get("status"))};return h(n.returnUrl),e([l]),!0}if(c){const e={code:i.get("error_code")||"oauth_error",message:i.get("error_description")||"OAuth authorization failed"};return h(n.returnUrl),t(e),!0}return!1}catch(e){return console.error("[OAuth Handler] Failed to check OAuth return:",e),sessionStorage.removeItem("alter_oauth_state"),!1}}openPopup(e){const t=window.screenX+(window.outerWidth-this.options.popupWidth)/2,s=window.screenY+(window.outerHeight-this.options.popupHeight)/2,n=[`width=${this.options.popupWidth}`,`height=${this.options.popupHeight}`,`left=${t}`,`top=${s}`,"resizable=yes","scrollbars=yes","status=yes"].join(",");this.log("Opening OAuth popup:",e),this.popup=window.open(e,"alter_oauth_popup",n),this.popup?(this.startPolling(),this.setupMessageListener()):this.options.onError({code:"popup_blocked",message:"Popup was blocked by browser. Please allow popups for this site."})}close(){this.log("Closing OAuth handler"),this.popup&&!this.popup.closed&&this.popup.close(),this.popup=null,null!==this.pollInterval&&(clearInterval(this.pollInterval),this.pollInterval=null),this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null)}startPolling(){this.pollInterval=window.setInterval(()=>{this.settled||this.popup&&!this.popup.closed||(this.log("Popup closed by user"),this.settled=!0,this.close(),this.options.onCancel())},500)}setupMessageListener(){this.messageListener=e=>{if(this.settled)return;if(e.origin!==this.expectedOrigin)return void this.log("Rejected message from unexpected origin:",e.origin,"(expected:",this.expectedOrigin+")");this.log("Received message from",e.origin);const t=e.data;if(t&&"object"==typeof t)if("alter_connect_success"===t.type){this.log("OAuth success");const e=Array.isArray(t.grants)?t.grants:null;if(e){this.log("Multi-provider success:",e.length,"grants");const t=[];for(const s of e){const e=s.grant_id;e&&s.provider?t.push({grant_id:e,provider:s.provider,provider_name:s.provider_name||s.provider,account_identifier:s.account_identifier||"",timestamp:s.timestamp||(new Date).toISOString(),operation:o(s.operation),scopes:Array.isArray(s.scopes)?s.scopes:[],status:a(s.status),metadata:s.metadata}):this.log("Skipping invalid grant item:",s)}return 0===t.length?(this.settled=!0,this.close(),void this.options.onError({code:"invalid_response",message:"Server returned empty grants array"})):(this.settled=!0,this.close(),void this.options.onSuccess(t))}this.settled=!0,this.close(),this.options.onError({code:"invalid_response",message:"Server returned success without grants array"})}else if("alter_connect_error"===t.type){this.log("OAuth error");const e={code:t.error||"oauth_error",message:t.error_description||"OAuth authorization failed",details:t};this.settled=!0,this.close(),this.options.onError(e)}},window.addEventListener("message",this.messageListener)}log(...e){this.options.debug&&console.log("[OAuth Handler]",...e)}}const l="0.2.0";let d=!1;class u{constructor(i={}){if(this._oauthHandler=null,this._perOpenCleanups=[],function(e){if(void 0!==e.baseURL)throw e.baseURL,new Error("AlterConnectConfig.baseURL is reserved and not yet supported. Omit the field to use the production Alter host.")}(i),this.config=function(e){return{debug:e.debug??!1}}(i),this._baseURL=(i.baseURL??e).replace(/\/+$/,""),this._baseURL!==e&&!d){d=!0;const e=new URL(this._baseURL).origin;console.warn(`[alter-connect] Using non-default Alter host: ${e}. Unset baseURL in AlterConnect.create() for production.`)}this.eventEmitter=new s,this.stateManager=new n,this._isInitialized=!0,t(this.config,"Alter Connect SDK initialized",{version:l,baseURL:this._baseURL}),this.checkRedirectReturn()}checkRedirectReturn(){c.checkOAuthReturn(e=>{t(this.config,"OAuth redirect return - success:",e),this.eventEmitter.emit("success",e)},e=>{t(this.config,"OAuth redirect return - error:",e),this.eventEmitter.emit("error",e)})&&t(this.config,"OAuth redirect return detected and handled")}static create(e){return new u(e)}async open(e){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call open() - SDK instance has been destroyed");if(t(this.config,"Opening Connect UI"),!e.token||"string"!=typeof e.token)throw this.createError("invalid_options","Session token is required. Create one from your backend using POST /sdk/oauth/connect/session");if(!e.onSuccess||"function"!=typeof e.onSuccess)throw this.createError("invalid_options","onSuccess callback is required");if(this.isOpen())return void t(this.config,"Connect UI is already open");this._oauthHandler=new c({baseURL:this._baseURL,onSuccess:e=>{t(this.config,"OAuth success:",e),this.handleOAuthSuccess(e)},onError:e=>{t(this.config,"OAuth error:",e),this.handleOAuthError(e)},onCancel:()=>{t(this.config,"OAuth cancelled (popup closed)"),this.handleOAuthCancel()},popupWidth:500,popupHeight:700,debug:this.config.debug}),this.stateManager.setState({isOpen:!0,error:null,sessionToken:e.token}),this.registerEventHandlers(e);const s=`${this._baseURL}/sdk/oauth/connect#session=${encodeURIComponent(e.token)}`;t(this.config,"Connect URL:",s),this._oauthHandler.startOAuth(s),e.onEvent&&e.onEvent("connect_opened",{timestamp:(new Date).toISOString()}),t(this.config,"Connect UI opened successfully")}close(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call close() - SDK instance has been destroyed");t(this.config,"Closing Connect UI"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this._perOpenCleanups.forEach(e=>e()),this._perOpenCleanups=[],this.stateManager.setState({isOpen:!1}),this.eventEmitter.emit("close")}destroy(){this._isInitialized&&(t(this.config,"Destroying SDK instance"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this.stateManager.get("isOpen")&&this.stateManager.setState({isOpen:!1}),this.eventEmitter.removeAllListeners(),this.stateManager.clearListeners(),this._isInitialized=!1)}on(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call on() - SDK instance has been destroyed");return this.eventEmitter.on(e,t)}off(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call off() - SDK instance has been destroyed");this.eventEmitter.off(e,t)}isOpen(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call isOpen() - SDK instance has been destroyed");return this.stateManager.get("isOpen")}getVersion(){return l}cleanupHandler(){this._oauthHandler=null}handleOAuthSuccess(e){this.cleanupHandler(),this.eventEmitter.emit("success",e)}handleOAuthError(e){this.cleanupHandler(),this.stateManager.setState({error:e}),this.eventEmitter.emit("error",e)}handleOAuthCancel(){this.cleanupHandler(),this.eventEmitter.emit("exit")}registerEventHandlers(e){e.onSuccess&&this._perOpenCleanups.push(this.eventEmitter.on("success",t=>{e.onSuccess(t),this.close()})),e.onExit&&this._perOpenCleanups.push(this.eventEmitter.on("exit",()=>{e.onExit(),this.close()})),this._perOpenCleanups.push(this.eventEmitter.on("error",t=>{e.onError&&e.onError(t),this.close()})),e.onEvent&&this._perOpenCleanups.push(this.eventEmitter.on("event",(t,s)=>{e.onEvent(t,s)}))}createError(e,t,s){const n=new Error(t);return n.code=e,n.details=s,n}}return u});
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).AlterConnect=t()}(this,function(){"use strict";const e="https://backend.alterauth.com";function t(e,...t){e.debug&&console.log("[Alter Connect]",...t)}class s{constructor(){this.events=new Map}on(e,t){return this.events.has(e)||this.events.set(e,new Set),this.events.get(e).add(t),()=>this.off(e,t)}off(e,t){const s=this.events.get(e);s&&s.delete(t)}emit(e,...t){const s=this.events.get(e);s&&s.forEach(s=>{try{s(...t)}catch(t){console.error(`[Alter Connect] Error in event handler for '${e}':`,t)}})}removeAllListeners(e){e?this.events.delete(e):this.events.clear()}}class n{constructor(){this.state={isOpen:!1,sessionToken:null,error:null},this.listeners=new Set}getState(){return{...this.state}}get(e){return this.state[e]}setState(e){this.state={...this.state,...e},this.notifyListeners()}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}clearListeners(){this.listeners.clear()}notifyListeners(){const e=this.getState();this.listeners.forEach(t=>{try{t(e)}catch(e){console.error("[Alter Connect] Error in state listener:",e)}})}}function i(){const e=navigator.userAgent||navigator.vendor||window.opera||"",t=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(e),s="ontouchstart"in window||navigator.maxTouchPoints>0||(navigator.msMaxTouchPoints??0)>0,n=window.innerWidth<=768;return t||s&&n}function r(){return i()&&window.innerWidth<=480?"phone":i()&&window.innerWidth>480&&window.innerWidth<=1024?"tablet":"desktop"}function o(e){return"reauth"===e?"reauth":"creation"}function a(e){return"pending"===e?"pending":"error"===e?"error":"active"}function h(e){sessionStorage.removeItem("alter_oauth_state");try{const t=new URL(e);window.history.replaceState({},document.title,t.pathname+t.search+t.hash)}catch{window.history.replaceState({},document.title,window.location.pathname)}}class c{constructor(t){this.popup=null,this.pollInterval=null,this.messageListener=null,this.settled=!1,this.options={baseURL:t.baseURL,onSuccess:t.onSuccess,onError:t.onError,onCancel:t.onCancel,popupWidth:t.popupWidth||500,popupHeight:t.popupHeight||700,debug:t.debug||!1,expectedOrigin:t.expectedOrigin||""};try{this.expectedOrigin=new URL(t.baseURL).origin}catch{throw new Error(`Invalid baseURL: "${t.baseURL}". Must be a full URL with protocol (e.g., "${e}").`)}}startOAuth(e){if(!this.options.expectedOrigin)try{const t=new URL(e);this.options.expectedOrigin=t.origin,this.log("Derived expected origin:",this.options.expectedOrigin)}catch{return this.log("Failed to parse OAuth URL for origin validation:",e),void this.options.onError({code:"invalid_oauth_url",message:"Failed to determine origin from OAuth URL. Cannot proceed securely."})}!function(){const e=r();return"phone"===e||"tablet"===e&&window.innerHeight>window.innerWidth}()?(this.log("Using popup flow for desktop"),this.openPopup(e)):(this.log("Using redirect flow for mobile device"),this.startRedirectFlow(e))}startRedirectFlow(e){this.log("Starting redirect flow:",e);const t={timestamp:Date.now(),returnUrl:window.location.href};try{sessionStorage.setItem("alter_oauth_state",JSON.stringify(t))}catch(e){return this.log("Failed to save state:",e),void this.options.onError({code:"redirect_error",message:"Failed to start OAuth flow: could not save session state",details:{error:e}})}window.location.href=e}static checkOAuthReturn(e,t){const s=sessionStorage.getItem("alter_oauth_state");if(!s)return!1;try{const n=JSON.parse(s);if(Date.now()-n.timestamp>3e5)return sessionStorage.removeItem("alter_oauth_state"),!1;const i=new URLSearchParams(window.location.search),r=i.get("alter_connect_success"),c=i.get("alter_connect_error");if("true"===r){const s=i.get("grant_id"),r=i.get("provider"),c=i.get("account_identifier");if(!s||!r||!c)return h(n.returnUrl),t({code:"invalid_response",message:"OAuth redirect returned incomplete grant data"}),!0;const l={grant_id:s,provider:r,provider_name:i.get("provider_name")||r,account_identifier:c,timestamp:i.get("timestamp")||(new Date).toISOString(),operation:o(i.get("operation")),scopes:i.get("scopes")?.split(",")||[],status:a(i.get("status"))};return h(n.returnUrl),e([l]),!0}if(c){const e={code:i.get("error_code")||"oauth_error",message:i.get("error_description")||"OAuth authorization failed"};return h(n.returnUrl),t(e),!0}return!1}catch(e){return console.error("[OAuth Handler] Failed to check OAuth return:",e),sessionStorage.removeItem("alter_oauth_state"),!1}}openPopup(e){const t=window.screenX+(window.outerWidth-this.options.popupWidth)/2,s=window.screenY+(window.outerHeight-this.options.popupHeight)/2,n=[`width=${this.options.popupWidth}`,`height=${this.options.popupHeight}`,`left=${t}`,`top=${s}`,"resizable=yes","scrollbars=yes","status=yes"].join(",");this.log("Opening OAuth popup:",e),this.popup=window.open(e,"alter_oauth_popup",n),this.popup?(this.startPolling(),this.setupMessageListener()):this.options.onError({code:"popup_blocked",message:"Popup was blocked by browser. Please allow popups for this site."})}close(){this.log("Closing OAuth handler"),this.popup&&!this.popup.closed&&this.popup.close(),this.popup=null,null!==this.pollInterval&&(clearInterval(this.pollInterval),this.pollInterval=null),this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null)}startPolling(){this.pollInterval=window.setInterval(()=>{this.settled||this.popup&&!this.popup.closed||(this.log("Popup closed by user"),this.settled=!0,this.close(),this.options.onCancel())},500)}setupMessageListener(){this.messageListener=e=>{if(this.settled)return;if(e.origin!==this.expectedOrigin)return void this.log("Rejected message from unexpected origin:",e.origin,"(expected:",this.expectedOrigin+")");this.log("Received message from",e.origin);const t=e.data;if(t&&"object"==typeof t)if("alter_connect_success"===t.type){this.log("OAuth success");const e=Array.isArray(t.grants)?t.grants:null;if(e){this.log("Multi-provider success:",e.length,"grants");const t=[];for(const s of e){const e=s.grant_id;e&&s.provider?t.push({grant_id:e,provider:s.provider,provider_name:s.provider_name||s.provider,account_identifier:s.account_identifier||"",timestamp:s.timestamp||(new Date).toISOString(),operation:o(s.operation),scopes:Array.isArray(s.scopes)?s.scopes:[],status:a(s.status),metadata:s.metadata}):this.log("Skipping invalid grant item:",s)}return 0===t.length?(this.settled=!0,this.close(),void this.options.onError({code:"invalid_response",message:"Server returned empty grants array"})):(this.settled=!0,this.close(),void this.options.onSuccess(t))}this.settled=!0,this.close(),this.options.onError({code:"invalid_response",message:"Server returned success without grants array"})}else if("alter_connect_error"===t.type){this.log("OAuth error");const e={code:t.error||"oauth_error",message:t.error_description||"OAuth authorization failed",details:t};this.settled=!0,this.close(),this.options.onError(e)}},window.addEventListener("message",this.messageListener)}log(...e){this.options.debug&&console.log("[OAuth Handler]",...e)}}const l="0.2.0";let d=!1;class u{constructor(i={}){if(this._oauthHandler=null,this._perOpenCleanups=[],function(e){if(void 0!==e.baseURL)throw e.baseURL,new Error("AlterConnectConfig.baseURL is reserved and not yet supported. Omit the field to use the production Alter host.")}(i),this.config=function(e){return{debug:e.debug??!1}}(i),this._baseURL=(i.baseURL??e).replace(/\/+$/,""),this._baseURL!==e&&!d){d=!0;const e=new URL(this._baseURL).origin;console.warn(`[alter-connect] Using non-default Alter host: ${e}. Unset baseURL in AlterConnect.create() for production.`)}this.eventEmitter=new s,this.stateManager=new n,this._isInitialized=!0,t(this.config,"Alter Connect SDK initialized",{version:l,baseURL:this._baseURL}),this.checkRedirectReturn()}checkRedirectReturn(){c.checkOAuthReturn(e=>{t(this.config,"OAuth redirect return - success:",e),this.eventEmitter.emit("success",e)},e=>{t(this.config,"OAuth redirect return - error:",e),this.eventEmitter.emit("error",e)})&&t(this.config,"OAuth redirect return detected and handled")}static create(e){return new u(e)}async open(e){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call open() - SDK instance has been destroyed");if(t(this.config,"Opening Connect UI"),!e.token||"string"!=typeof e.token)throw this.createError("invalid_options","Session token is required. Create one from your backend using POST /sdk/oauth/connect/session");if(!e.onSuccess||"function"!=typeof e.onSuccess)throw this.createError("invalid_options","onSuccess callback is required");if(this.isOpen())return void t(this.config,"Connect UI is already open");this._oauthHandler=new c({baseURL:this._baseURL,onSuccess:e=>{t(this.config,"OAuth success:",e),this.handleOAuthSuccess(e)},onError:e=>{t(this.config,"OAuth error:",e),this.handleOAuthError(e)},onCancel:()=>{t(this.config,"OAuth cancelled (popup closed)"),this.handleOAuthCancel()},popupWidth:500,popupHeight:700,debug:this.config.debug}),this.stateManager.setState({isOpen:!0,error:null,sessionToken:e.token}),this.registerEventHandlers(e);const s=`${this._baseURL}/sdk/oauth/connect#session=${encodeURIComponent(e.token)}`;t(this.config,"Connect URL:",s),this._oauthHandler.startOAuth(s),e.onEvent&&e.onEvent("connect_opened",{timestamp:(new Date).toISOString()}),t(this.config,"Connect UI opened successfully")}close(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call close() - SDK instance has been destroyed");t(this.config,"Closing Connect UI"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this._perOpenCleanups.forEach(e=>e()),this._perOpenCleanups=[],this.stateManager.setState({isOpen:!1}),this.eventEmitter.emit("close")}destroy(){this._isInitialized&&(t(this.config,"Destroying SDK instance"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this.stateManager.get("isOpen")&&this.stateManager.setState({isOpen:!1}),this.eventEmitter.removeAllListeners(),this.stateManager.clearListeners(),this._isInitialized=!1)}on(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call on() - SDK instance has been destroyed");return this.eventEmitter.on(e,t)}off(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call off() - SDK instance has been destroyed");this.eventEmitter.off(e,t)}isOpen(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call isOpen() - SDK instance has been destroyed");return this.stateManager.get("isOpen")}getVersion(){return l}cleanupHandler(){this._oauthHandler=null}handleOAuthSuccess(e){this.cleanupHandler(),this.eventEmitter.emit("success",e)}handleOAuthError(e){this.cleanupHandler(),this.stateManager.setState({error:e}),this.eventEmitter.emit("error",e)}handleOAuthCancel(){this.cleanupHandler(),this.eventEmitter.emit("exit")}registerEventHandlers(e){e.onSuccess&&this._perOpenCleanups.push(this.eventEmitter.on("success",t=>{e.onSuccess(t),this.close()})),e.onExit&&this._perOpenCleanups.push(this.eventEmitter.on("exit",()=>{e.onExit(),this.close()})),this._perOpenCleanups.push(this.eventEmitter.on("error",t=>{e.onError&&e.onError(t),this.close()})),e.onEvent&&this._perOpenCleanups.push(this.eventEmitter.on("event",(t,s)=>{e.onEvent(t,s)}))}createError(e,t,s){const n=new Error(t);return n.code=e,n.details=s,n}}return u});
|
|
2
2
|
//# sourceMappingURL=alter-connect.umd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"alter-connect.umd.js","sources":["../src/core/config.ts","../src/core/events.ts","../src/state/manager.ts","../src/utils/mobile.ts","../src/oauth/handler.ts","../src/core/alter-connect.ts"],"sourcesContent":["/**\n * Configuration validation and management\n */\n\nimport type { AlterConnectConfig } from '../types';\n\n/**\n * Canonical Alter Connect host.\n *\n * Single source of truth for the widget's default host. `alter-connect.ts`\n * uses it as the constructor fallback; `config.ts` and `oauth/handler.ts`\n * surface it in error messages so operators always see the same example\n * URL no matter which validation layer flags their misconfig.\n */\nexport const DEFAULT_ALTER_HOST = 'https://backend.alterauth.com';\n\n/**\n * Internal config used by the SDK\n */\ninterface FullConfig {\n debug: boolean;\n}\n\n/**\n * Validate configuration.\n *\n * `baseURL` is RESERVED in the public type for future self-hosted /\n * non-production support. It is not yet accepted at runtime — see the\n * `REMOVE_TO_ENABLE_BASE_URL_OVERRIDE` block below. The deep validation\n * beneath it is intentionally kept so re-enabling is a single-line\n * deletion; each check retains its inline rationale so future-readers\n * don't have to reconstruct the reasoning.\n */\nconst ALLOWED_BASE_URL_PROTOCOLS = ['http:', 'https:'];\n\nexport function validateConfig(config: AlterConnectConfig): void {\n if (config.baseURL !== undefined) {\n // Capture into a locally-typed const so the unreachable block below\n // still type-checks after the early throw (TS loses the narrowing\n // from `config.baseURL !== undefined` across the throw boundary).\n const baseURL: string = config.baseURL;\n\n // --- REMOVE_TO_ENABLE_BASE_URL_OVERRIDE ---\n // Delete this throw to enable baseURL override support. The deep\n // validation below is ready to accept any well-formed http/https\n // URL without userinfo, query, or fragment. Keep the throw message\n // free of the supplied value — callers may pass strings carrying\n // secrets, and thrown errors flow into error-tracking integrations.\n throw new Error(\n 'AlterConnectConfig.baseURL is reserved and not yet supported. Omit the field to use the production Alter host.'\n );\n // --- END REMOVE_TO_ENABLE_BASE_URL_OVERRIDE ---\n\n // Everything below is unreachable today (guarded by the throw above)\n // but preserved so re-enabling the override is a one-line change.\n // `eslint-disable-next-line` only silences the one following\n // statement, so every subsequent unreachable statement in this\n // block would re-trigger `no-unreachable`. Wrap the whole block\n // with an enable/disable pair instead.\n /* eslint-disable no-unreachable */\n if (typeof baseURL !== 'string' || baseURL.trim() === '') {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: must be a non-empty string. Omit the field to use the default Alter host.'\n );\n }\n if (baseURL !== baseURL.trim()) {\n // URL parsers silently trim ASCII whitespace but we store the raw\n // string on the instance and string-concat it into popup URLs, so\n // a padded value produces malformed popups. Reject loudly instead.\n //\n // Intentionally do NOT echo the offending value — a misconfigured\n // string could sit next to secret material, and thrown errors flow\n // into error-tracking integrations (Sentry, Datadog RUM, etc.).\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: contains leading or trailing whitespace.'\n );\n }\n let parsed: URL;\n try {\n parsed = new URL(baseURL);\n } catch {\n // Do NOT echo baseURL — a malformed string can still carry secret\n // material (e.g. query tokens that slipped past the whitespace\n // check) that would flow into error trackers via the thrown\n // message. The error tells the operator which rule was violated;\n // they can inspect their own config to debug.\n throw new Error(\n `Invalid AlterConnectConfig.baseURL: must be a full URL with protocol (e.g., \"${DEFAULT_ALTER_HOST}\").`\n );\n }\n // Reject every field that could carry secret material BEFORE any\n // downstream code echoes the URL string. userinfo = inline basic-auth\n // credentials, search = query-string tokens, hash = session tokens.\n // A base URL is just a host (+ optional path prefix) by definition —\n // if the operator needs any of these, they're passing the wrong value.\n if (parsed.username !== '' || parsed.password !== '') {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: credentials embedded in the URL (user:password@) are not allowed. Remove them from the URL and provide credentials through the normal auth flow.'\n );\n }\n if (parsed.search !== '') {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: query strings are not allowed on a base URL. Remove the \"?...\" portion.'\n );\n }\n if (parsed.hash !== '') {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: URL fragments are not allowed on a base URL. Remove the \"#...\" portion.'\n );\n }\n if (!ALLOWED_BASE_URL_PROTOCOLS.includes(parsed.protocol)) {\n // Do NOT echo baseURL — at this point we've rejected userinfo,\n // query, and fragment, so the remaining secret-leak risk is low,\n // but echoing is inconsistent with the other validation branches\n // and isn't needed to convey the rule to the operator.\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: only http:// and https:// are allowed.'\n );\n }\n /* eslint-enable no-unreachable */\n }\n}\n\n/**\n * Merge user config with defaults\n */\nexport function mergeConfig(config: AlterConnectConfig): FullConfig {\n return {\n debug: config.debug ?? false,\n };\n}\n\n/**\n * Debug logging helper\n */\nexport function debugLog(config: FullConfig, ...args: any[]): void {\n if (config.debug) {\n console.log('[Alter Connect]', ...args);\n }\n}\n\n/**\n * Export FullConfig type for use in other modules\n */\nexport type { FullConfig };\n","/**\n * Simple event emitter for SDK events\n */\n\ntype EventHandler = (...args: any[]) => void;\n\n/**\n * Event emitter for SDK internal events\n */\nexport class EventEmitter {\n private events: Map<string, Set<EventHandler>>;\n\n constructor() {\n this.events = new Map();\n }\n\n /**\n * Register an event listener\n * @returns Unsubscribe function\n */\n on(event: string, handler: EventHandler): () => void {\n if (!this.events.has(event)) {\n this.events.set(event, new Set());\n }\n\n this.events.get(event)!.add(handler);\n\n // Return unsubscribe function\n return () => this.off(event, handler);\n }\n\n /**\n * Remove an event listener\n */\n off(event: string, handler: EventHandler): void {\n const handlers = this.events.get(event);\n if (handlers) {\n handlers.delete(handler);\n }\n }\n\n /**\n * Emit an event\n */\n emit(event: string, ...args: any[]): void {\n const handlers = this.events.get(event);\n if (handlers) {\n handlers.forEach(handler => {\n try {\n handler(...args);\n } catch (error) {\n console.error(`[Alter Connect] Error in event handler for '${event}':`, error);\n }\n });\n }\n }\n\n /**\n * Remove all listeners for an event, or all listeners if no event specified\n */\n removeAllListeners(event?: string): void {\n if (event) {\n this.events.delete(event);\n } else {\n this.events.clear();\n }\n }\n}\n","/**\n * State management for SDK\n */\n\nimport type { AlterError } from '../types';\n\n/**\n * SDK internal state\n */\nexport interface SDKState {\n isOpen: boolean;\n sessionToken: string | null;\n error: AlterError | null;\n}\n\ntype StateListener = (state: SDKState) => void;\n\n/**\n * Simple state manager\n */\nexport class StateManager {\n private state: SDKState;\n private listeners: Set<StateListener>;\n\n constructor() {\n this.state = {\n isOpen: false,\n sessionToken: null,\n error: null,\n };\n this.listeners = new Set();\n }\n\n /**\n * Get current state\n */\n getState(): Readonly<SDKState> {\n return { ...this.state };\n }\n\n /**\n * Get a specific state value\n */\n get<K extends keyof SDKState>(key: K): SDKState[K] {\n return this.state[key];\n }\n\n /**\n * Update state\n */\n setState(updates: Partial<SDKState>): void {\n this.state = { ...this.state, ...updates };\n this.notifyListeners();\n }\n\n /**\n * Subscribe to state changes\n */\n subscribe(listener: StateListener): () => void {\n this.listeners.add(listener);\n\n // Return unsubscribe function\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Clear all listeners\n */\n clearListeners(): void {\n this.listeners.clear();\n }\n\n /**\n * Notify all listeners of state change\n */\n private notifyListeners(): void {\n const currentState = this.getState();\n this.listeners.forEach(listener => {\n try {\n listener(currentState);\n } catch (error) {\n console.error('[Alter Connect] Error in state listener:', error);\n }\n });\n }\n}\n","/**\n * Mobile device detection and responsive utilities\n */\n\n/**\n * Detect if the current device is a mobile device\n */\nexport function isMobileDevice(): boolean {\n const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;\n\n // Check for mobile patterns\n const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;\n const isMobileUA = mobileRegex.test(userAgent);\n\n // Check for touch capability\n const hasTouchScreen = ('ontouchstart' in window) ||\n (navigator.maxTouchPoints > 0) ||\n ((navigator as any).msMaxTouchPoints > 0);\n\n // Check screen size\n const isSmallScreen = window.innerWidth <= 768;\n\n return isMobileUA || (hasTouchScreen && isSmallScreen);\n}\n\n/**\n * Detect if the device is specifically a phone (vs tablet)\n */\nexport function isPhoneDevice(): boolean {\n return isMobileDevice() && window.innerWidth <= 480;\n}\n\n/**\n * Detect if the device is a tablet\n */\nexport function isTabletDevice(): boolean {\n return isMobileDevice() && window.innerWidth > 480 && window.innerWidth <= 1024;\n}\n\n/**\n * Get the device type\n */\nexport function getDeviceType(): 'desktop' | 'tablet' | 'phone' {\n if (isPhoneDevice()) return 'phone';\n if (isTabletDevice()) return 'tablet';\n return 'desktop';\n}\n\n/**\n * Check if we should use redirect flow instead of popup\n *\n * Popup windows don't work well on mobile:\n * - Browsers often block them\n * - They open as full-screen tabs\n * - Users lose context of parent app\n */\nexport function shouldUseRedirectFlow(): boolean {\n const deviceType = getDeviceType();\n\n // Always use redirect for phones\n if (deviceType === 'phone') return true;\n\n // Use redirect for tablets in portrait mode (popups are cramped)\n if (deviceType === 'tablet' && window.innerHeight > window.innerWidth) {\n return true;\n }\n\n // Use popup for desktop and tablets in landscape\n return false;\n}\n","/**\n * OAuth popup and redirect handler\n */\n\nimport type { Grant, AlterError } from '../types';\nimport { DEFAULT_ALTER_HOST } from '../core/config';\nimport { shouldUseRedirectFlow } from '../utils/mobile';\n\ninterface OAuthHandlerOptions {\n baseURL: string;\n onSuccess: (grants: Grant[]) => void;\n onError: (error: AlterError) => void;\n onCancel: () => void;\n popupWidth?: number;\n popupHeight?: number;\n debug?: boolean;\n /** Expected origin for postMessage validation. Derived from the OAuth URL. */\n expectedOrigin?: string;\n}\n\n/**\n * Parse an operation string into a valid Grant['operation'] value\n */\nfunction parseOperation(value: string | null | undefined): Grant['operation'] {\n if (value === 'reauth') return 'reauth';\n return 'creation';\n}\n\n/**\n * Parse a status string into a valid Grant['status'] value\n */\nfunction parseStatus(value: string | null | undefined): Grant['status'] {\n if (value === 'pending') return 'pending';\n if (value === 'error') return 'error';\n return 'active';\n}\n\n/**\n * Clean up OAuth redirect state and restore the original URL\n */\nfunction cleanupRedirectState(returnUrl: string): void {\n sessionStorage.removeItem('alter_oauth_state');\n try {\n const cleanUrl = new URL(returnUrl);\n window.history.replaceState({}, document.title, cleanUrl.pathname + cleanUrl.search + cleanUrl.hash);\n } catch {\n // If returnUrl is malformed, just remove query params from current URL\n window.history.replaceState({}, document.title, window.location.pathname);\n }\n}\n\n/**\n * Handles OAuth flow in popup window\n */\nexport class OAuthHandler {\n private popup: Window | null = null;\n private pollInterval: number | null = null;\n private messageListener: ((event: MessageEvent) => void) | null = null;\n private options: Required<OAuthHandlerOptions>;\n private expectedOrigin: string;\n private settled: boolean = false;\n\n constructor(options: OAuthHandlerOptions) {\n this.options = {\n baseURL: options.baseURL,\n onSuccess: options.onSuccess,\n onError: options.onError,\n onCancel: options.onCancel,\n popupWidth: options.popupWidth || 500,\n popupHeight: options.popupHeight || 700,\n debug: options.debug || false,\n expectedOrigin: options.expectedOrigin || '',\n };\n\n // Extract origin from baseURL for postMessage validation\n try {\n this.expectedOrigin = new URL(options.baseURL).origin;\n } catch {\n throw new Error(\n `Invalid baseURL: \"${options.baseURL}\". Must be a full URL with protocol (e.g., \"${DEFAULT_ALTER_HOST}\").`\n );\n }\n }\n\n /**\n * Start OAuth flow (automatically chooses popup or redirect based on device)\n */\n startOAuth(url: string): void {\n // Derive expected origin from the OAuth URL if not explicitly set.\n // SECURITY: Fail-closed — if we can't determine origin, abort the flow.\n if (!this.options.expectedOrigin) {\n try {\n const parsed = new URL(url);\n this.options.expectedOrigin = parsed.origin;\n this.log('Derived expected origin:', this.options.expectedOrigin);\n } catch {\n this.log('Failed to parse OAuth URL for origin validation:', url);\n this.options.onError({\n code: 'invalid_oauth_url',\n message: 'Failed to determine origin from OAuth URL. Cannot proceed securely.',\n });\n return;\n }\n }\n\n if (shouldUseRedirectFlow()) {\n this.log('Using redirect flow for mobile device');\n this.startRedirectFlow(url);\n } else {\n this.log('Using popup flow for desktop');\n this.openPopup(url);\n }\n }\n\n /**\n * Start OAuth redirect flow (for mobile)\n * Saves state and redirects the entire page\n */\n private startRedirectFlow(url: string): void {\n this.log('Starting redirect flow:', url);\n\n // Save state to sessionStorage to restore after redirect\n const state = {\n timestamp: Date.now(),\n returnUrl: window.location.href,\n };\n\n try {\n sessionStorage.setItem('alter_oauth_state', JSON.stringify(state));\n } catch (error) {\n this.log('Failed to save state:', error);\n this.options.onError({\n code: 'redirect_error',\n message: 'Failed to start OAuth flow: could not save session state',\n details: { error },\n });\n return;\n }\n\n // Redirect to OAuth URL (full page redirect)\n window.location.href = url;\n }\n\n /**\n * Check if returning from OAuth redirect and handle result\n * Call this on page load to detect OAuth return\n *\n * @returns true if OAuth return was detected and handled\n */\n static checkOAuthReturn(\n onSuccess: (grants: Grant[]) => void,\n onError: (error: AlterError) => void\n ): boolean {\n // Check if we have OAuth state saved\n const stateJson = sessionStorage.getItem('alter_oauth_state');\n if (!stateJson) {\n return false;\n }\n\n try {\n const state = JSON.parse(stateJson);\n\n // Check if this is a valid OAuth return (within 5 minutes)\n const elapsed = Date.now() - state.timestamp;\n if (elapsed > 5 * 60 * 1000) {\n // Stale state, clean up\n sessionStorage.removeItem('alter_oauth_state');\n return false;\n }\n\n // Check URL for OAuth callback parameters\n const urlParams = new URLSearchParams(window.location.search);\n const success = urlParams.get('alter_connect_success');\n const error = urlParams.get('alter_connect_error');\n\n if (success === 'true') {\n // Validate required fields\n const grantId = urlParams.get('grant_id');\n const provider = urlParams.get('provider');\n const accountIdentifier = urlParams.get('account_identifier');\n\n if (!grantId || !provider || !accountIdentifier) {\n cleanupRedirectState(state.returnUrl);\n onError({\n code: 'invalid_response',\n message: 'OAuth redirect returned incomplete grant data',\n });\n return true;\n }\n\n const grant: Grant = {\n grant_id: grantId,\n provider: provider,\n provider_name: urlParams.get('provider_name') || provider,\n account_identifier: accountIdentifier,\n timestamp: urlParams.get('timestamp') || new Date().toISOString(),\n operation: parseOperation(urlParams.get('operation')),\n scopes: urlParams.get('scopes')?.split(',') || [],\n status: parseStatus(urlParams.get('status')),\n };\n\n cleanupRedirectState(state.returnUrl);\n onSuccess([grant]); // Wrap in array for unified interface\n return true;\n }\n\n if (error) {\n const alterError: AlterError = {\n code: urlParams.get('error_code') || 'oauth_error',\n message: urlParams.get('error_description') || 'OAuth authorization failed',\n };\n\n cleanupRedirectState(state.returnUrl);\n onError(alterError);\n return true;\n }\n\n return false;\n } catch (err) {\n console.error('[OAuth Handler] Failed to check OAuth return:', err);\n sessionStorage.removeItem('alter_oauth_state');\n return false;\n }\n }\n\n /**\n * Open OAuth popup (for desktop)\n */\n openPopup(url: string): void {\n // Calculate popup position (centered)\n const left = window.screenX + (window.outerWidth - this.options.popupWidth) / 2;\n const top = window.screenY + (window.outerHeight - this.options.popupHeight) / 2;\n\n const features = [\n `width=${this.options.popupWidth}`,\n `height=${this.options.popupHeight}`,\n `left=${left}`,\n `top=${top}`,\n 'resizable=yes',\n 'scrollbars=yes',\n 'status=yes',\n ].join(',');\n\n this.log('Opening OAuth popup:', url);\n\n // Open popup\n this.popup = window.open(url, 'alter_oauth_popup', features);\n\n if (!this.popup) {\n this.options.onError({\n code: 'popup_blocked',\n message: 'Popup was blocked by browser. Please allow popups for this site.',\n });\n return;\n }\n\n // Start monitoring popup\n this.startPolling();\n this.setupMessageListener();\n }\n\n /**\n * Close popup and clean up\n */\n close(): void {\n this.log('Closing OAuth handler');\n\n // Close popup\n if (this.popup && !this.popup.closed) {\n this.popup.close();\n }\n this.popup = null;\n\n // Stop polling\n if (this.pollInterval !== null) {\n clearInterval(this.pollInterval);\n this.pollInterval = null;\n }\n\n // Remove message listener\n if (this.messageListener) {\n window.removeEventListener('message', this.messageListener);\n this.messageListener = null;\n }\n }\n\n /**\n * Poll to detect when popup is closed\n */\n private startPolling(): void {\n this.pollInterval = window.setInterval(() => {\n if (this.settled) return;\n if (!this.popup || this.popup.closed) {\n this.log('Popup closed by user');\n this.settled = true;\n this.close();\n this.options.onCancel();\n }\n }, 500);\n }\n\n /**\n * Listen for postMessage from OAuth callback page\n */\n private setupMessageListener(): void {\n this.messageListener = (event: MessageEvent) => {\n // Ignore if already settled (prevents race with polling)\n if (this.settled) return;\n\n // Validate origin against the expected backend origin\n if (event.origin !== this.expectedOrigin) {\n this.log('Rejected message from unexpected origin:', event.origin, '(expected:', this.expectedOrigin + ')');\n return;\n }\n\n this.log('Received message from', event.origin);\n\n const data = event.data;\n\n // Check message type\n if (data && typeof data === 'object') {\n if (data.type === 'alter_connect_success') {\n this.log('OAuth success');\n\n // Accept both grants array and legacy connections array\n const rawGrants = Array.isArray(data.grants) ? data.grants : null;\n if (rawGrants) {\n this.log('Multi-provider success:', rawGrants.length, 'grants');\n const grants: Grant[] = [];\n for (const item of rawGrants) {\n const grantId = item.grant_id as string | undefined;\n if (!grantId || !item.provider) {\n this.log('Skipping invalid grant item:', item);\n continue;\n }\n grants.push({\n grant_id: grantId,\n provider: item.provider as string,\n provider_name: (item.provider_name as string) || (item.provider as string),\n account_identifier: (item.account_identifier as string) || '',\n timestamp: (item.timestamp as string) || new Date().toISOString(),\n operation: parseOperation(item.operation as string | undefined),\n scopes: Array.isArray(item.scopes) ? item.scopes as string[] : [],\n status: parseStatus(item.status as string | undefined),\n metadata: item.metadata as Grant['metadata'],\n });\n }\n\n if (grants.length === 0) {\n this.settled = true;\n this.close();\n this.options.onError({\n code: 'invalid_response',\n message: 'Server returned empty grants array',\n });\n return;\n }\n\n this.settled = true;\n this.close();\n this.options.onSuccess(grants);\n return;\n }\n\n // No valid grants array found\n this.settled = true;\n this.close();\n this.options.onError({\n code: 'invalid_response',\n message: 'Server returned success without grants array',\n });\n } else if (data.type === 'alter_connect_error') {\n this.log('OAuth error');\n\n const error: AlterError = {\n code: (data.error as string) || 'oauth_error',\n message: (data.error_description as string) || 'OAuth authorization failed',\n details: data as Record<string, unknown>,\n };\n\n this.settled = true;\n this.close();\n this.options.onError(error);\n }\n }\n };\n\n window.addEventListener('message', this.messageListener);\n }\n\n /**\n * Log debug message\n */\n private log(...args: unknown[]): void {\n if (this.options.debug) {\n console.log('[OAuth Handler]', ...args);\n }\n }\n}\n","/**\n * Main AlterConnect SDK class\n *\n * Opens the backend-served Connect UI in a popup. The backend handles\n * provider selection, branding, and OAuth — then sends the result back via postMessage.\n */\n\nimport type { AlterConnectConfig, OpenOptions, Grant, AlterError } from '../types';\nimport { validateConfig, mergeConfig, debugLog, DEFAULT_ALTER_HOST, type FullConfig } from './config';\nimport { EventEmitter } from './events';\nimport { StateManager } from '../state/manager';\nimport { OAuthHandler } from '../oauth/handler';\n\n/**\n * SDK version\n */\nconst VERSION = '0.2.0';\n\n/**\n * Module-scoped flag so the non-default-host warning fires at most once\n * per process. Reset is intentionally not exposed — the warning exists to\n * surface accidental misconfigurations during dev, not to spam logs.\n */\nlet hasWarnedNonDefaultHost = false;\n\n/**\n * Main SDK class\n */\nexport class AlterConnect {\n private config: FullConfig;\n private eventEmitter: EventEmitter;\n private stateManager: StateManager;\n private _isInitialized: boolean;\n private _oauthHandler: OAuthHandler | null = null;\n private readonly _baseURL: string;\n private _perOpenCleanups: Array<() => void> = [];\n\n /**\n * Private constructor (use AlterConnect.create() instead)\n */\n private constructor(config: AlterConnectConfig = {}) {\n // Validate and merge config\n validateConfig(config);\n this.config = mergeConfig(config);\n\n // Resolve the Alter host once at construction time. `baseURL` is an\n // environment-level setting (production vs self-hosted vs local-dev),\n // not a per-`open()` decision — that's why it lives on the constructor\n // config and not on OpenOptions.\n this._baseURL = (config.baseURL ?? DEFAULT_ALTER_HOST).replace(/\\/+$/, '');\n if (this._baseURL !== DEFAULT_ALTER_HOST && !hasWarnedNonDefaultHost) {\n hasWarnedNonDefaultHost = true;\n // Log only the origin, not the full URL. validateConfig rejects\n // userinfo/search/hash, so the path is the only \"extra\" piece the\n // origin strips — but it costs us nothing to be defensive here in\n // case validateConfig's allowlist is ever broadened. The warn\n // message is forwarded to customer-side error trackers (Sentry,\n // Datadog RUM), so minimizing what flows through it is prudent.\n const origin = new URL(this._baseURL).origin;\n console.warn(\n `[alter-connect] Using non-default Alter host: ${origin}. ` +\n 'Unset baseURL in AlterConnect.create() for production.'\n );\n }\n\n // Initialize components\n this.eventEmitter = new EventEmitter();\n this.stateManager = new StateManager();\n this._isInitialized = true;\n\n debugLog(this.config, 'Alter Connect SDK initialized', { version: VERSION, baseURL: this._baseURL });\n\n // Check if returning from OAuth redirect (mobile flow)\n this.checkRedirectReturn();\n }\n\n /**\n * Check if returning from OAuth redirect and handle result\n * This runs automatically on SDK initialization\n */\n private checkRedirectReturn(): void {\n const handled = OAuthHandler.checkOAuthReturn(\n (grants: Grant[]) => {\n debugLog(this.config, 'OAuth redirect return - success:', grants);\n // Emit success event (will be picked up by event handlers if SDK was re-initialized)\n this.eventEmitter.emit('success', grants);\n },\n (error: AlterError) => {\n debugLog(this.config, 'OAuth redirect return - error:', error);\n // Emit error event\n this.eventEmitter.emit('error', error);\n }\n );\n\n if (handled) {\n debugLog(this.config, 'OAuth redirect return detected and handled');\n }\n }\n\n /**\n * Create a new AlterConnect instance\n *\n * @param config Optional configuration\n * @returns AlterConnect instance\n *\n * @example\n * const alterConnect = AlterConnect.create();\n * const alterConnect = AlterConnect.create({ debug: true });\n */\n static create(config?: AlterConnectConfig): AlterConnect {\n return new AlterConnect(config);\n }\n\n /**\n * Open the Connect UI in a popup window\n *\n * Opens the backend-served Connect UI at {baseURL}/oauth/connect#session={token}.\n * The backend handles provider selection, branding, and OAuth flow.\n * On success, the backend sends a postMessage back to this window.\n *\n * @param options Configuration including the session token from your backend\n *\n * @example\n * // 1. Get session token from YOUR backend\n * const { session_token } = await fetch('/api/alter/create-session').then(r => r.json());\n *\n * // 2. Open Connect UI\n * await alterConnect.open({\n * token: session_token,\n * onSuccess: (grants) => {\n * console.log('Connected!', grants);\n * },\n * onError: (error) => {\n * console.error('Failed:', error);\n * }\n * });\n */\n async open(options: OpenOptions): Promise<void> {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call open() - SDK instance has been destroyed');\n }\n\n debugLog(this.config, 'Opening Connect UI');\n\n // Validate required options\n if (!options.token || typeof options.token !== 'string') {\n throw this.createError('invalid_options', 'Session token is required. Create one from your backend using POST /sdk/oauth/connect/session');\n }\n\n if (!options.onSuccess || typeof options.onSuccess !== 'function') {\n throw this.createError('invalid_options', 'onSuccess callback is required');\n }\n\n // Check if already open\n if (this.isOpen()) {\n debugLog(this.config, 'Connect UI is already open');\n return;\n }\n\n // Create OAuth handler first — this validates baseURL and throws on invalid input.\n // We do this BEFORE setting isOpen to avoid leaving the SDK in a broken state.\n this._oauthHandler = new OAuthHandler({\n baseURL: this._baseURL,\n onSuccess: (grants: Grant[]) => {\n debugLog(this.config, 'OAuth success:', grants);\n this.handleOAuthSuccess(grants);\n },\n onError: (error: AlterError) => {\n debugLog(this.config, 'OAuth error:', error);\n this.handleOAuthError(error);\n },\n onCancel: () => {\n debugLog(this.config, 'OAuth cancelled (popup closed)');\n this.handleOAuthCancel();\n },\n popupWidth: 500,\n popupHeight: 700,\n debug: this.config.debug,\n });\n\n // Update state (after handler creation succeeds)\n this.stateManager.setState({\n isOpen: true,\n error: null,\n sessionToken: options.token,\n });\n\n // Register event handlers\n this.registerEventHandlers(options);\n\n // Build the Connect UI URL — the backend serves the full UI\n const connectURL = `${this._baseURL}/sdk/oauth/connect#session=${encodeURIComponent(options.token)}`;\n\n debugLog(this.config, 'Connect URL:', connectURL);\n\n // Open the Connect UI (popup or redirect depending on device)\n this._oauthHandler.startOAuth(connectURL);\n\n // Emit analytics event\n if (options.onEvent) {\n options.onEvent('connect_opened', {\n timestamp: new Date().toISOString(),\n });\n }\n\n debugLog(this.config, 'Connect UI opened successfully');\n }\n\n /**\n * Close the Connect UI\n */\n close(): void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call close() - SDK instance has been destroyed');\n }\n\n debugLog(this.config, 'Closing Connect UI');\n\n // Clean up OAuth handler if active\n if (this._oauthHandler) {\n this._oauthHandler.close();\n this._oauthHandler = null;\n }\n\n // Remove only per-open handlers (preserves global handlers from on())\n this._perOpenCleanups.forEach(cleanup => cleanup());\n this._perOpenCleanups = [];\n\n // Update state\n this.stateManager.setState({ isOpen: false });\n\n this.eventEmitter.emit('close');\n }\n\n /**\n * Destroy the SDK instance and clean up resources\n */\n destroy(): void {\n if (!this._isInitialized) {\n // Already destroyed, silently return\n return;\n }\n\n debugLog(this.config, 'Destroying SDK instance');\n\n // Clean up OAuth handler if active\n if (this._oauthHandler) {\n this._oauthHandler.close();\n this._oauthHandler = null;\n }\n\n // Update state\n if (this.stateManager.get('isOpen')) {\n this.stateManager.setState({ isOpen: false });\n }\n\n // Clean up resources\n this.eventEmitter.removeAllListeners();\n this.stateManager.clearListeners();\n this._isInitialized = false;\n }\n\n /**\n * Register an event listener\n */\n on(event: string, handler: (...args: any[]) => void): () => void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call on() - SDK instance has been destroyed');\n }\n\n return this.eventEmitter.on(event, handler);\n }\n\n /**\n * Remove an event listener\n */\n off(event: string, handler: (...args: any[]) => void): void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call off() - SDK instance has been destroyed');\n }\n\n this.eventEmitter.off(event, handler);\n }\n\n /**\n * Check if Connect UI is open\n */\n isOpen(): boolean {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError('sdk_destroyed', 'Cannot call isOpen() - SDK instance has been destroyed');\n }\n\n return this.stateManager.get('isOpen');\n }\n\n /**\n * Get SDK version\n */\n getVersion(): string {\n return VERSION;\n }\n\n /**\n * Clean up OAuth handler reference after a flow completes.\n * The OAuthHandler.close() is already called internally by the handler\n * before invoking callbacks, so we only need to null the reference here.\n */\n private cleanupHandler(): void {\n this._oauthHandler = null;\n }\n\n /**\n * Handle OAuth success\n */\n private handleOAuthSuccess(grants: Grant[]): void {\n this.cleanupHandler();\n this.eventEmitter.emit('success', grants);\n }\n\n /**\n * Handle OAuth error\n */\n private handleOAuthError(error: AlterError): void {\n this.cleanupHandler();\n this.stateManager.setState({ error });\n this.eventEmitter.emit('error', error);\n }\n\n /**\n * Handle OAuth cancellation (user closed popup)\n */\n private handleOAuthCancel(): void {\n this.cleanupHandler();\n this.eventEmitter.emit('exit');\n }\n\n /**\n * Register event handlers from OpenOptions\n */\n private registerEventHandlers(options: OpenOptions): void {\n // Success handler\n if (options.onSuccess) {\n this._perOpenCleanups.push(\n this.eventEmitter.on('success', (grants: Grant[]) => {\n options.onSuccess(grants);\n this.close();\n })\n );\n }\n\n // Exit handler\n if (options.onExit) {\n this._perOpenCleanups.push(\n this.eventEmitter.on('exit', () => {\n options.onExit!();\n this.close();\n })\n );\n }\n\n // Error handler — always registered to ensure close() is called\n this._perOpenCleanups.push(\n this.eventEmitter.on('error', (error: AlterError) => {\n if (options.onError) {\n options.onError(error);\n }\n this.close();\n })\n );\n\n // Event handler (analytics)\n if (options.onEvent) {\n this._perOpenCleanups.push(\n this.eventEmitter.on('event', (eventName: string, metadata: any) => {\n options.onEvent!(eventName, metadata);\n })\n );\n }\n }\n\n /**\n * Create an AlterError object\n */\n private createError(code: string, message: string, details?: Record<string, any>): Error {\n const error = new Error(message);\n (error as any).code = code;\n (error as any).details = details;\n return error;\n }\n}\n"],"names":["DEFAULT_ALTER_HOST","debugLog","config","args","debug","console","log","EventEmitter","constructor","this","events","Map","on","event","handler","has","set","Set","get","add","off","handlers","delete","emit","forEach","error","removeAllListeners","clear","StateManager","state","isOpen","sessionToken","listeners","getState","key","setState","updates","notifyListeners","subscribe","listener","clearListeners","currentState","isMobileDevice","userAgent","navigator","vendor","window","opera","isMobileUA","test","hasTouchScreen","maxTouchPoints","msMaxTouchPoints","isSmallScreen","innerWidth","getDeviceType","parseOperation","value","parseStatus","cleanupRedirectState","returnUrl","sessionStorage","removeItem","cleanUrl","URL","history","replaceState","document","title","pathname","search","hash","location","OAuthHandler","options","popup","pollInterval","messageListener","settled","baseURL","onSuccess","onError","onCancel","popupWidth","popupHeight","expectedOrigin","origin","Error","startOAuth","url","parsed","code","message","deviceType","innerHeight","shouldUseRedirectFlow","openPopup","startRedirectFlow","timestamp","Date","now","href","setItem","JSON","stringify","details","checkOAuthReturn","stateJson","getItem","parse","urlParams","URLSearchParams","success","grantId","provider","accountIdentifier","grant","grant_id","provider_name","account_identifier","toISOString","operation","scopes","split","status","alterError","err","left","screenX","outerWidth","top","screenY","outerHeight","features","join","open","startPolling","setupMessageListener","close","closed","clearInterval","removeEventListener","setInterval","data","type","rawGrants","Array","isArray","grants","length","item","push","metadata","error_description","addEventListener","VERSION","hasWarnedNonDefaultHost","AlterConnect","_oauthHandler","_perOpenCleanups","undefined","validateConfig","mergeConfig","_baseURL","replace","warn","eventEmitter","stateManager","_isInitialized","version","checkRedirectReturn","create","createError","token","handleOAuthSuccess","handleOAuthError","handleOAuthCancel","registerEventHandlers","connectURL","encodeURIComponent","onEvent","cleanup","destroy","getVersion","cleanupHandler","onExit","eventName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"alter-connect.umd.js","sources":["../src/core/config.ts","../src/core/events.ts","../src/state/manager.ts","../src/utils/mobile.ts","../src/oauth/handler.ts","../src/core/alter-connect.ts"],"sourcesContent":["/**\n * Configuration validation and management\n */\n\nimport type { AlterConnectConfig } from \"../types\";\n\n/**\n * Canonical Alter Connect host.\n *\n * Single source of truth for the widget's default host. `alter-connect.ts`\n * uses it as the constructor fallback; `config.ts` and `oauth/handler.ts`\n * surface it in error messages so operators always see the same example\n * URL no matter which validation layer flags their misconfig.\n */\nexport const DEFAULT_ALTER_HOST = \"https://backend.alterauth.com\";\n\n/**\n * Internal config used by the SDK\n */\ninterface FullConfig {\n debug: boolean;\n}\n\n/**\n * Validate configuration.\n *\n * `baseURL` is RESERVED in the public type for future self-hosted /\n * non-production support. It is not yet accepted at runtime — see the\n * `REMOVE_TO_ENABLE_BASE_URL_OVERRIDE` block below. The deep validation\n * beneath it is intentionally kept so re-enabling is a single-line\n * deletion; each check retains its inline rationale so future-readers\n * don't have to reconstruct the reasoning.\n */\nconst ALLOWED_BASE_URL_PROTOCOLS = [\"http:\", \"https:\"];\n\nexport function validateConfig(config: AlterConnectConfig): void {\n if (config.baseURL !== undefined) {\n // Capture into a locally-typed const so the unreachable block below\n // still type-checks after the early throw (TS loses the narrowing\n // from `config.baseURL !== undefined` across the throw boundary).\n const baseURL: string = config.baseURL;\n\n // --- REMOVE_TO_ENABLE_BASE_URL_OVERRIDE ---\n // Delete this throw to enable baseURL override support. The deep\n // validation below is ready to accept any well-formed http/https\n // URL without userinfo, query, or fragment. Keep the throw message\n // free of the supplied value — callers may pass strings carrying\n // secrets, and thrown errors flow into error-tracking integrations.\n throw new Error(\n \"AlterConnectConfig.baseURL is reserved and not yet supported. Omit the field to use the production Alter host.\",\n );\n // --- END REMOVE_TO_ENABLE_BASE_URL_OVERRIDE ---\n\n // Everything below is unreachable today (guarded by the throw above)\n // but preserved so re-enabling the override is a one-line change.\n // This package enables `no-unreachable` (unreachable code is almost\n // always a bug), so this ONE deliberate case carries an explicit\n // disable/enable pair — deleting the throw above must also delete it.\n /* eslint-disable no-unreachable */\n if (typeof baseURL !== \"string\" || baseURL.trim() === \"\") {\n throw new Error(\n \"Invalid AlterConnectConfig.baseURL: must be a non-empty string. Omit the field to use the default Alter host.\",\n );\n }\n if (baseURL !== baseURL.trim()) {\n // URL parsers silently trim ASCII whitespace but we store the raw\n // string on the instance and string-concat it into popup URLs, so\n // a padded value produces malformed popups. Reject loudly instead.\n //\n // Intentionally do NOT echo the offending value — a misconfigured\n // string could sit next to secret material, and thrown errors flow\n // into error-tracking integrations (Sentry, Datadog RUM, etc.).\n throw new Error(\n \"Invalid AlterConnectConfig.baseURL: contains leading or trailing whitespace.\",\n );\n }\n let parsed: URL;\n try {\n parsed = new URL(baseURL);\n } catch {\n // Do NOT echo baseURL — a malformed string can still carry secret\n // material (e.g. query tokens that slipped past the whitespace\n // check) that would flow into error trackers via the thrown\n // message. The error tells the operator which rule was violated;\n // they can inspect their own config to debug.\n throw new Error(\n `Invalid AlterConnectConfig.baseURL: must be a full URL with protocol (e.g., \"${DEFAULT_ALTER_HOST}\").`,\n );\n }\n // Reject every field that could carry secret material BEFORE any\n // downstream code echoes the URL string. userinfo = inline basic-auth\n // credentials, search = query-string tokens, hash = session tokens.\n // A base URL is just a host (+ optional path prefix) by definition —\n // if the operator needs any of these, they're passing the wrong value.\n if (parsed.username !== \"\" || parsed.password !== \"\") {\n throw new Error(\n \"Invalid AlterConnectConfig.baseURL: credentials embedded in the URL (user:password@) are not allowed. Remove them from the URL and provide credentials through the normal auth flow.\",\n );\n }\n if (parsed.search !== \"\") {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: query strings are not allowed on a base URL. Remove the \"?...\" portion.',\n );\n }\n if (parsed.hash !== \"\") {\n throw new Error(\n 'Invalid AlterConnectConfig.baseURL: URL fragments are not allowed on a base URL. Remove the \"#...\" portion.',\n );\n }\n if (!ALLOWED_BASE_URL_PROTOCOLS.includes(parsed.protocol)) {\n // Do NOT echo baseURL — at this point we've rejected userinfo,\n // query, and fragment, so the remaining secret-leak risk is low,\n // but echoing is inconsistent with the other validation branches\n // and isn't needed to convey the rule to the operator.\n throw new Error(\n \"Invalid AlterConnectConfig.baseURL: only http:// and https:// are allowed.\",\n );\n }\n /* eslint-enable no-unreachable */\n }\n}\n\n/**\n * Merge user config with defaults\n */\nexport function mergeConfig(config: AlterConnectConfig): FullConfig {\n return {\n debug: config.debug ?? false,\n };\n}\n\n/**\n * Debug logging helper\n */\nexport function debugLog(config: FullConfig, ...args: unknown[]): void {\n if (config.debug) {\n // Deliberate console.log: this IS the debug logger, and it only fires\n // when the integrator explicitly opts in via ``config.debug``.\n // eslint-disable-next-line no-console\n console.log(\"[Alter Connect]\", ...args);\n }\n}\n\n/**\n * Export FullConfig type for use in other modules\n */\nexport type { FullConfig };\n","/**\n * Simple event emitter for SDK events\n */\n\n// Variadic ``never[]`` keeps registration contravariant-safe under\n// strictFunctionTypes: any concretely-typed listener (e.g.\n// ``(name: string) => void``) is assignable, while the emitter itself\n// invokes handlers through an ``unknown[]`` cast in emit().\nexport type EventHandler = (...args: never[]) => void;\n\n/**\n * Event emitter for SDK internal events\n */\nexport class EventEmitter {\n private events: Map<string, Set<EventHandler>>;\n\n constructor() {\n this.events = new Map();\n }\n\n /**\n * Register an event listener\n * @returns Unsubscribe function\n */\n on(event: string, handler: EventHandler): () => void {\n if (!this.events.has(event)) {\n this.events.set(event, new Set());\n }\n\n this.events.get(event)!.add(handler);\n\n // Return unsubscribe function\n return () => this.off(event, handler);\n }\n\n /**\n * Remove an event listener\n */\n off(event: string, handler: EventHandler): void {\n const handlers = this.events.get(event);\n if (handlers) {\n handlers.delete(handler);\n }\n }\n\n /**\n * Emit an event\n */\n emit(event: string, ...args: unknown[]): void {\n const handlers = this.events.get(event);\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n (handler as (...emitted: unknown[]) => void)(...args);\n } catch (error) {\n console.error(\n `[Alter Connect] Error in event handler for '${event}':`,\n error,\n );\n }\n });\n }\n }\n\n /**\n * Remove all listeners for an event, or all listeners if no event specified\n */\n removeAllListeners(event?: string): void {\n if (event) {\n this.events.delete(event);\n } else {\n this.events.clear();\n }\n }\n}\n","/**\n * State management for SDK\n */\n\nimport type { AlterError } from \"../types\";\n\n/**\n * SDK internal state\n */\nexport interface SDKState {\n isOpen: boolean;\n sessionToken: string | null;\n error: AlterError | null;\n}\n\ntype StateListener = (state: SDKState) => void;\n\n/**\n * Simple state manager\n */\nexport class StateManager {\n private state: SDKState;\n private listeners: Set<StateListener>;\n\n constructor() {\n this.state = {\n isOpen: false,\n sessionToken: null,\n error: null,\n };\n this.listeners = new Set();\n }\n\n /**\n * Get current state\n */\n getState(): Readonly<SDKState> {\n return { ...this.state };\n }\n\n /**\n * Get a specific state value\n */\n get<K extends keyof SDKState>(key: K): SDKState[K] {\n return this.state[key];\n }\n\n /**\n * Update state\n */\n setState(updates: Partial<SDKState>): void {\n this.state = { ...this.state, ...updates };\n this.notifyListeners();\n }\n\n /**\n * Subscribe to state changes\n */\n subscribe(listener: StateListener): () => void {\n this.listeners.add(listener);\n\n // Return unsubscribe function\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Clear all listeners\n */\n clearListeners(): void {\n this.listeners.clear();\n }\n\n /**\n * Notify all listeners of state change\n */\n private notifyListeners(): void {\n const currentState = this.getState();\n this.listeners.forEach((listener) => {\n try {\n listener(currentState);\n } catch (error) {\n console.error(\"[Alter Connect] Error in state listener:\", error);\n }\n });\n }\n}\n","/**\n * Mobile device detection and responsive utilities\n */\n\n/**\n * Detect if the current device is a mobile device\n */\nexport function isMobileDevice(): boolean {\n // ``window.opera`` (legacy Opera) and ``navigator.msMaxTouchPoints``\n // (IE/old Edge) are non-standard vendor fields absent from lib.dom —\n // narrow structural casts instead of ``any``.\n const userAgent =\n navigator.userAgent ||\n navigator.vendor ||\n (window as Window & { opera?: string }).opera ||\n \"\";\n\n // Check for mobile patterns\n const mobileRegex =\n /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;\n const isMobileUA = mobileRegex.test(userAgent);\n\n // Check for touch capability\n const hasTouchScreen =\n \"ontouchstart\" in window ||\n navigator.maxTouchPoints > 0 ||\n ((navigator as Navigator & { msMaxTouchPoints?: number })\n .msMaxTouchPoints ?? 0) > 0;\n\n // Check screen size\n const isSmallScreen = window.innerWidth <= 768;\n\n return isMobileUA || (hasTouchScreen && isSmallScreen);\n}\n\n/**\n * Detect if the device is specifically a phone (vs tablet)\n */\nexport function isPhoneDevice(): boolean {\n return isMobileDevice() && window.innerWidth <= 480;\n}\n\n/**\n * Detect if the device is a tablet\n */\nexport function isTabletDevice(): boolean {\n return (\n isMobileDevice() && window.innerWidth > 480 && window.innerWidth <= 1024\n );\n}\n\n/**\n * Get the device type\n */\nexport function getDeviceType(): \"desktop\" | \"tablet\" | \"phone\" {\n if (isPhoneDevice()) return \"phone\";\n if (isTabletDevice()) return \"tablet\";\n return \"desktop\";\n}\n\n/**\n * Check if we should use redirect flow instead of popup\n *\n * Popup windows don't work well on mobile:\n * - Browsers often block them\n * - They open as full-screen tabs\n * - Users lose context of parent app\n */\nexport function shouldUseRedirectFlow(): boolean {\n const deviceType = getDeviceType();\n\n // Always use redirect for phones\n if (deviceType === \"phone\") return true;\n\n // Use redirect for tablets in portrait mode (popups are cramped)\n if (deviceType === \"tablet\" && window.innerHeight > window.innerWidth) {\n return true;\n }\n\n // Use popup for desktop and tablets in landscape\n return false;\n}\n","/**\n * OAuth popup and redirect handler\n */\n\nimport type { Grant, AlterError } from \"../types\";\nimport { DEFAULT_ALTER_HOST } from \"../core/config\";\nimport { shouldUseRedirectFlow } from \"../utils/mobile\";\n\ninterface OAuthHandlerOptions {\n baseURL: string;\n onSuccess: (grants: Grant[]) => void;\n onError: (error: AlterError) => void;\n onCancel: () => void;\n popupWidth?: number;\n popupHeight?: number;\n debug?: boolean;\n /** Expected origin for postMessage validation. Derived from the OAuth URL. */\n expectedOrigin?: string;\n}\n\n/**\n * Parse an operation string into a valid Grant['operation'] value\n */\nfunction parseOperation(value: string | null | undefined): Grant[\"operation\"] {\n if (value === \"reauth\") return \"reauth\";\n return \"creation\";\n}\n\n/**\n * Parse a status string into a valid Grant['status'] value\n */\nfunction parseStatus(value: string | null | undefined): Grant[\"status\"] {\n if (value === \"pending\") return \"pending\";\n if (value === \"error\") return \"error\";\n return \"active\";\n}\n\n/**\n * Clean up OAuth redirect state and restore the original URL\n */\nfunction cleanupRedirectState(returnUrl: string): void {\n sessionStorage.removeItem(\"alter_oauth_state\");\n try {\n const cleanUrl = new URL(returnUrl);\n window.history.replaceState(\n {},\n document.title,\n cleanUrl.pathname + cleanUrl.search + cleanUrl.hash,\n );\n } catch {\n // If returnUrl is malformed, just remove query params from current URL\n window.history.replaceState({}, document.title, window.location.pathname);\n }\n}\n\n/**\n * Handles OAuth flow in popup window\n */\nexport class OAuthHandler {\n private popup: Window | null = null;\n private pollInterval: number | null = null;\n private messageListener: ((event: MessageEvent) => void) | null = null;\n private options: Required<OAuthHandlerOptions>;\n private expectedOrigin: string;\n private settled: boolean = false;\n\n constructor(options: OAuthHandlerOptions) {\n this.options = {\n baseURL: options.baseURL,\n onSuccess: options.onSuccess,\n onError: options.onError,\n onCancel: options.onCancel,\n popupWidth: options.popupWidth || 500,\n popupHeight: options.popupHeight || 700,\n debug: options.debug || false,\n expectedOrigin: options.expectedOrigin || \"\",\n };\n\n // Extract origin from baseURL for postMessage validation\n try {\n this.expectedOrigin = new URL(options.baseURL).origin;\n } catch {\n throw new Error(\n `Invalid baseURL: \"${options.baseURL}\". Must be a full URL with protocol (e.g., \"${DEFAULT_ALTER_HOST}\").`,\n );\n }\n }\n\n /**\n * Start OAuth flow (automatically chooses popup or redirect based on device)\n */\n startOAuth(url: string): void {\n // Derive expected origin from the OAuth URL if not explicitly set.\n // SECURITY: Fail-closed — if we can't determine origin, abort the flow.\n if (!this.options.expectedOrigin) {\n try {\n const parsed = new URL(url);\n this.options.expectedOrigin = parsed.origin;\n this.log(\"Derived expected origin:\", this.options.expectedOrigin);\n } catch {\n this.log(\"Failed to parse OAuth URL for origin validation:\", url);\n this.options.onError({\n code: \"invalid_oauth_url\",\n message:\n \"Failed to determine origin from OAuth URL. Cannot proceed securely.\",\n });\n return;\n }\n }\n\n if (shouldUseRedirectFlow()) {\n this.log(\"Using redirect flow for mobile device\");\n this.startRedirectFlow(url);\n } else {\n this.log(\"Using popup flow for desktop\");\n this.openPopup(url);\n }\n }\n\n /**\n * Start OAuth redirect flow (for mobile)\n * Saves state and redirects the entire page\n */\n private startRedirectFlow(url: string): void {\n this.log(\"Starting redirect flow:\", url);\n\n // Save state to sessionStorage to restore after redirect\n const state = {\n timestamp: Date.now(),\n returnUrl: window.location.href,\n };\n\n try {\n sessionStorage.setItem(\"alter_oauth_state\", JSON.stringify(state));\n } catch (error) {\n this.log(\"Failed to save state:\", error);\n this.options.onError({\n code: \"redirect_error\",\n message: \"Failed to start OAuth flow: could not save session state\",\n details: { error },\n });\n return;\n }\n\n // Redirect to OAuth URL (full page redirect)\n window.location.href = url;\n }\n\n /**\n * Check if returning from OAuth redirect and handle result\n * Call this on page load to detect OAuth return\n *\n * @returns true if OAuth return was detected and handled\n */\n static checkOAuthReturn(\n onSuccess: (grants: Grant[]) => void,\n onError: (error: AlterError) => void,\n ): boolean {\n // Check if we have OAuth state saved\n const stateJson = sessionStorage.getItem(\"alter_oauth_state\");\n if (!stateJson) {\n return false;\n }\n\n try {\n const state = JSON.parse(stateJson);\n\n // Check if this is a valid OAuth return (within 5 minutes)\n const elapsed = Date.now() - state.timestamp;\n if (elapsed > 5 * 60 * 1000) {\n // Stale state, clean up\n sessionStorage.removeItem(\"alter_oauth_state\");\n return false;\n }\n\n // Check URL for OAuth callback parameters\n const urlParams = new URLSearchParams(window.location.search);\n const success = urlParams.get(\"alter_connect_success\");\n const error = urlParams.get(\"alter_connect_error\");\n\n if (success === \"true\") {\n // Validate required fields\n const grantId = urlParams.get(\"grant_id\");\n const provider = urlParams.get(\"provider\");\n const accountIdentifier = urlParams.get(\"account_identifier\");\n\n if (!grantId || !provider || !accountIdentifier) {\n cleanupRedirectState(state.returnUrl);\n onError({\n code: \"invalid_response\",\n message: \"OAuth redirect returned incomplete grant data\",\n });\n return true;\n }\n\n const grant: Grant = {\n grant_id: grantId,\n provider: provider,\n provider_name: urlParams.get(\"provider_name\") || provider,\n account_identifier: accountIdentifier,\n timestamp: urlParams.get(\"timestamp\") || new Date().toISOString(),\n operation: parseOperation(urlParams.get(\"operation\")),\n scopes: urlParams.get(\"scopes\")?.split(\",\") || [],\n status: parseStatus(urlParams.get(\"status\")),\n };\n\n cleanupRedirectState(state.returnUrl);\n onSuccess([grant]); // Wrap in array for unified interface\n return true;\n }\n\n if (error) {\n const alterError: AlterError = {\n code: urlParams.get(\"error_code\") || \"oauth_error\",\n message:\n urlParams.get(\"error_description\") || \"OAuth authorization failed\",\n };\n\n cleanupRedirectState(state.returnUrl);\n onError(alterError);\n return true;\n }\n\n return false;\n } catch (err) {\n console.error(\"[OAuth Handler] Failed to check OAuth return:\", err);\n sessionStorage.removeItem(\"alter_oauth_state\");\n return false;\n }\n }\n\n /**\n * Open OAuth popup (for desktop)\n */\n openPopup(url: string): void {\n // Calculate popup position (centered)\n const left =\n window.screenX + (window.outerWidth - this.options.popupWidth) / 2;\n const top =\n window.screenY + (window.outerHeight - this.options.popupHeight) / 2;\n\n const features = [\n `width=${this.options.popupWidth}`,\n `height=${this.options.popupHeight}`,\n `left=${left}`,\n `top=${top}`,\n \"resizable=yes\",\n \"scrollbars=yes\",\n \"status=yes\",\n ].join(\",\");\n\n this.log(\"Opening OAuth popup:\", url);\n\n // Open popup\n this.popup = window.open(url, \"alter_oauth_popup\", features);\n\n if (!this.popup) {\n this.options.onError({\n code: \"popup_blocked\",\n message:\n \"Popup was blocked by browser. Please allow popups for this site.\",\n });\n return;\n }\n\n // Start monitoring popup\n this.startPolling();\n this.setupMessageListener();\n }\n\n /**\n * Close popup and clean up\n */\n close(): void {\n this.log(\"Closing OAuth handler\");\n\n // Close popup\n if (this.popup && !this.popup.closed) {\n this.popup.close();\n }\n this.popup = null;\n\n // Stop polling\n if (this.pollInterval !== null) {\n clearInterval(this.pollInterval);\n this.pollInterval = null;\n }\n\n // Remove message listener\n if (this.messageListener) {\n window.removeEventListener(\"message\", this.messageListener);\n this.messageListener = null;\n }\n }\n\n /**\n * Poll to detect when popup is closed\n */\n private startPolling(): void {\n this.pollInterval = window.setInterval(() => {\n if (this.settled) return;\n if (!this.popup || this.popup.closed) {\n this.log(\"Popup closed by user\");\n this.settled = true;\n this.close();\n this.options.onCancel();\n }\n }, 500);\n }\n\n /**\n * Listen for postMessage from OAuth callback page\n */\n private setupMessageListener(): void {\n this.messageListener = (event: MessageEvent) => {\n // Ignore if already settled (prevents race with polling)\n if (this.settled) return;\n\n // Validate origin against the expected backend origin\n if (event.origin !== this.expectedOrigin) {\n this.log(\n \"Rejected message from unexpected origin:\",\n event.origin,\n \"(expected:\",\n this.expectedOrigin + \")\",\n );\n return;\n }\n\n this.log(\"Received message from\", event.origin);\n\n const data = event.data;\n\n // Check message type\n if (data && typeof data === \"object\") {\n if (data.type === \"alter_connect_success\") {\n this.log(\"OAuth success\");\n\n // Accept both grants array and legacy connections array\n const rawGrants = Array.isArray(data.grants) ? data.grants : null;\n if (rawGrants) {\n this.log(\"Multi-provider success:\", rawGrants.length, \"grants\");\n const grants: Grant[] = [];\n for (const item of rawGrants) {\n const grantId = item.grant_id as string | undefined;\n if (!grantId || !item.provider) {\n this.log(\"Skipping invalid grant item:\", item);\n continue;\n }\n grants.push({\n grant_id: grantId,\n provider: item.provider as string,\n provider_name:\n (item.provider_name as string) || (item.provider as string),\n account_identifier: (item.account_identifier as string) || \"\",\n timestamp:\n (item.timestamp as string) || new Date().toISOString(),\n operation: parseOperation(item.operation as string | undefined),\n scopes: Array.isArray(item.scopes)\n ? (item.scopes as string[])\n : [],\n status: parseStatus(item.status as string | undefined),\n metadata: item.metadata as Grant[\"metadata\"],\n });\n }\n\n if (grants.length === 0) {\n this.settled = true;\n this.close();\n this.options.onError({\n code: \"invalid_response\",\n message: \"Server returned empty grants array\",\n });\n return;\n }\n\n this.settled = true;\n this.close();\n this.options.onSuccess(grants);\n return;\n }\n\n // No valid grants array found\n this.settled = true;\n this.close();\n this.options.onError({\n code: \"invalid_response\",\n message: \"Server returned success without grants array\",\n });\n } else if (data.type === \"alter_connect_error\") {\n this.log(\"OAuth error\");\n\n const error: AlterError = {\n code: (data.error as string) || \"oauth_error\",\n message:\n (data.error_description as string) ||\n \"OAuth authorization failed\",\n details: data as Record<string, unknown>,\n };\n\n this.settled = true;\n this.close();\n this.options.onError(error);\n }\n }\n };\n\n window.addEventListener(\"message\", this.messageListener);\n }\n\n /**\n * Log debug message\n */\n private log(...args: unknown[]): void {\n if (this.options.debug) {\n // Deliberate console.log: this IS the debug logger, and it only fires\n // when the integrator explicitly opts in via ``debug``.\n // eslint-disable-next-line no-console\n console.log(\"[OAuth Handler]\", ...args);\n }\n }\n}\n","/**\n * Main AlterConnect SDK class\n *\n * Opens the backend-served Connect UI in a popup. The backend handles\n * provider selection, branding, and OAuth — then sends the result back via postMessage.\n */\n\nimport type {\n AlterConnectConfig,\n OpenOptions,\n Grant,\n AlterError,\n} from \"../types\";\nimport {\n validateConfig,\n mergeConfig,\n debugLog,\n DEFAULT_ALTER_HOST,\n type FullConfig,\n} from \"./config\";\nimport { EventEmitter, type EventHandler } from \"./events\";\nimport { StateManager } from \"../state/manager\";\nimport { OAuthHandler } from \"../oauth/handler\";\n\n/**\n * SDK version\n */\nconst VERSION = \"0.2.0\";\n\n/**\n * Module-scoped flag so the non-default-host warning fires at most once\n * per process. Reset is intentionally not exposed — the warning exists to\n * surface accidental misconfigurations during dev, not to spam logs.\n */\nlet hasWarnedNonDefaultHost = false;\n\n/**\n * Main SDK class\n */\nexport class AlterConnect {\n private config: FullConfig;\n private eventEmitter: EventEmitter;\n private stateManager: StateManager;\n private _isInitialized: boolean;\n private _oauthHandler: OAuthHandler | null = null;\n private readonly _baseURL: string;\n private _perOpenCleanups: Array<() => void> = [];\n\n /**\n * Private constructor (use AlterConnect.create() instead)\n */\n private constructor(config: AlterConnectConfig = {}) {\n // Validate and merge config\n validateConfig(config);\n this.config = mergeConfig(config);\n\n // Resolve the Alter host once at construction time. `baseURL` is an\n // environment-level setting (production vs self-hosted vs local-dev),\n // not a per-`open()` decision — that's why it lives on the constructor\n // config and not on OpenOptions.\n this._baseURL = (config.baseURL ?? DEFAULT_ALTER_HOST).replace(/\\/+$/, \"\");\n if (this._baseURL !== DEFAULT_ALTER_HOST && !hasWarnedNonDefaultHost) {\n hasWarnedNonDefaultHost = true;\n // Log only the origin, not the full URL. validateConfig rejects\n // userinfo/search/hash, so the path is the only \"extra\" piece the\n // origin strips — but it costs us nothing to be defensive here in\n // case validateConfig's allowlist is ever broadened. The warn\n // message is forwarded to customer-side error trackers (Sentry,\n // Datadog RUM), so minimizing what flows through it is prudent.\n const origin = new URL(this._baseURL).origin;\n console.warn(\n `[alter-connect] Using non-default Alter host: ${origin}. ` +\n \"Unset baseURL in AlterConnect.create() for production.\",\n );\n }\n\n // Initialize components\n this.eventEmitter = new EventEmitter();\n this.stateManager = new StateManager();\n this._isInitialized = true;\n\n debugLog(this.config, \"Alter Connect SDK initialized\", {\n version: VERSION,\n baseURL: this._baseURL,\n });\n\n // Check if returning from OAuth redirect (mobile flow)\n this.checkRedirectReturn();\n }\n\n /**\n * Check if returning from OAuth redirect and handle result\n * This runs automatically on SDK initialization\n */\n private checkRedirectReturn(): void {\n const handled = OAuthHandler.checkOAuthReturn(\n (grants: Grant[]) => {\n debugLog(this.config, \"OAuth redirect return - success:\", grants);\n // Emit success event (will be picked up by event handlers if SDK was re-initialized)\n this.eventEmitter.emit(\"success\", grants);\n },\n (error: AlterError) => {\n debugLog(this.config, \"OAuth redirect return - error:\", error);\n // Emit error event\n this.eventEmitter.emit(\"error\", error);\n },\n );\n\n if (handled) {\n debugLog(this.config, \"OAuth redirect return detected and handled\");\n }\n }\n\n /**\n * Create a new AlterConnect instance\n *\n * @param config Optional configuration\n * @returns AlterConnect instance\n *\n * @example\n * const alterConnect = AlterConnect.create();\n * const alterConnect = AlterConnect.create({ debug: true });\n */\n static create(config?: AlterConnectConfig): AlterConnect {\n return new AlterConnect(config);\n }\n\n /**\n * Open the Connect UI in a popup window\n *\n * Opens the backend-served Connect UI at {baseURL}/oauth/connect#session={token}.\n * The backend handles provider selection, branding, and OAuth flow.\n * On success, the backend sends a postMessage back to this window.\n *\n * @param options Configuration including the session token from your backend\n *\n * @example\n * // 1. Get session token from YOUR backend\n * const { session_token } = await fetch('/api/alter/create-session').then(r => r.json());\n *\n * // 2. Open Connect UI\n * await alterConnect.open({\n * token: session_token,\n * onSuccess: (grants) => {\n * console.log('Connected!', grants);\n * },\n * onError: (error) => {\n * console.error('Failed:', error);\n * }\n * });\n */\n async open(options: OpenOptions): Promise<void> {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call open() - SDK instance has been destroyed\",\n );\n }\n\n debugLog(this.config, \"Opening Connect UI\");\n\n // Validate required options\n if (!options.token || typeof options.token !== \"string\") {\n throw this.createError(\n \"invalid_options\",\n \"Session token is required. Create one from your backend using POST /sdk/oauth/connect/session\",\n );\n }\n\n if (!options.onSuccess || typeof options.onSuccess !== \"function\") {\n throw this.createError(\n \"invalid_options\",\n \"onSuccess callback is required\",\n );\n }\n\n // Check if already open\n if (this.isOpen()) {\n debugLog(this.config, \"Connect UI is already open\");\n return;\n }\n\n // Create OAuth handler first — this validates baseURL and throws on invalid input.\n // We do this BEFORE setting isOpen to avoid leaving the SDK in a broken state.\n this._oauthHandler = new OAuthHandler({\n baseURL: this._baseURL,\n onSuccess: (grants: Grant[]) => {\n debugLog(this.config, \"OAuth success:\", grants);\n this.handleOAuthSuccess(grants);\n },\n onError: (error: AlterError) => {\n debugLog(this.config, \"OAuth error:\", error);\n this.handleOAuthError(error);\n },\n onCancel: () => {\n debugLog(this.config, \"OAuth cancelled (popup closed)\");\n this.handleOAuthCancel();\n },\n popupWidth: 500,\n popupHeight: 700,\n debug: this.config.debug,\n });\n\n // Update state (after handler creation succeeds)\n this.stateManager.setState({\n isOpen: true,\n error: null,\n sessionToken: options.token,\n });\n\n // Register event handlers\n this.registerEventHandlers(options);\n\n // Build the Connect UI URL — the backend serves the full UI\n const connectURL = `${this._baseURL}/sdk/oauth/connect#session=${encodeURIComponent(options.token)}`;\n\n debugLog(this.config, \"Connect URL:\", connectURL);\n\n // Open the Connect UI (popup or redirect depending on device)\n this._oauthHandler.startOAuth(connectURL);\n\n // Emit analytics event\n if (options.onEvent) {\n options.onEvent(\"connect_opened\", {\n timestamp: new Date().toISOString(),\n });\n }\n\n debugLog(this.config, \"Connect UI opened successfully\");\n }\n\n /**\n * Close the Connect UI\n */\n close(): void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call close() - SDK instance has been destroyed\",\n );\n }\n\n debugLog(this.config, \"Closing Connect UI\");\n\n // Clean up OAuth handler if active\n if (this._oauthHandler) {\n this._oauthHandler.close();\n this._oauthHandler = null;\n }\n\n // Remove only per-open handlers (preserves global handlers from on())\n this._perOpenCleanups.forEach((cleanup) => cleanup());\n this._perOpenCleanups = [];\n\n // Update state\n this.stateManager.setState({ isOpen: false });\n\n this.eventEmitter.emit(\"close\");\n }\n\n /**\n * Destroy the SDK instance and clean up resources\n */\n destroy(): void {\n if (!this._isInitialized) {\n // Already destroyed, silently return\n return;\n }\n\n debugLog(this.config, \"Destroying SDK instance\");\n\n // Clean up OAuth handler if active\n if (this._oauthHandler) {\n this._oauthHandler.close();\n this._oauthHandler = null;\n }\n\n // Update state\n if (this.stateManager.get(\"isOpen\")) {\n this.stateManager.setState({ isOpen: false });\n }\n\n // Clean up resources\n this.eventEmitter.removeAllListeners();\n this.stateManager.clearListeners();\n this._isInitialized = false;\n }\n\n /**\n * Register an event listener\n */\n on(event: string, handler: EventHandler): () => void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call on() - SDK instance has been destroyed\",\n );\n }\n\n return this.eventEmitter.on(event, handler);\n }\n\n /**\n * Remove an event listener\n */\n off(event: string, handler: EventHandler): void {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call off() - SDK instance has been destroyed\",\n );\n }\n\n this.eventEmitter.off(event, handler);\n }\n\n /**\n * Check if Connect UI is open\n */\n isOpen(): boolean {\n // Check if SDK has been destroyed\n if (!this._isInitialized) {\n throw this.createError(\n \"sdk_destroyed\",\n \"Cannot call isOpen() - SDK instance has been destroyed\",\n );\n }\n\n return this.stateManager.get(\"isOpen\");\n }\n\n /**\n * Get SDK version\n */\n getVersion(): string {\n return VERSION;\n }\n\n /**\n * Clean up OAuth handler reference after a flow completes.\n * The OAuthHandler.close() is already called internally by the handler\n * before invoking callbacks, so we only need to null the reference here.\n */\n private cleanupHandler(): void {\n this._oauthHandler = null;\n }\n\n /**\n * Handle OAuth success\n */\n private handleOAuthSuccess(grants: Grant[]): void {\n this.cleanupHandler();\n this.eventEmitter.emit(\"success\", grants);\n }\n\n /**\n * Handle OAuth error\n */\n private handleOAuthError(error: AlterError): void {\n this.cleanupHandler();\n this.stateManager.setState({ error });\n this.eventEmitter.emit(\"error\", error);\n }\n\n /**\n * Handle OAuth cancellation (user closed popup)\n */\n private handleOAuthCancel(): void {\n this.cleanupHandler();\n this.eventEmitter.emit(\"exit\");\n }\n\n /**\n * Register event handlers from OpenOptions\n */\n private registerEventHandlers(options: OpenOptions): void {\n // Success handler\n if (options.onSuccess) {\n this._perOpenCleanups.push(\n this.eventEmitter.on(\"success\", (grants: Grant[]) => {\n options.onSuccess(grants);\n this.close();\n }),\n );\n }\n\n // Exit handler\n if (options.onExit) {\n this._perOpenCleanups.push(\n this.eventEmitter.on(\"exit\", () => {\n options.onExit!();\n this.close();\n }),\n );\n }\n\n // Error handler — always registered to ensure close() is called\n this._perOpenCleanups.push(\n this.eventEmitter.on(\"error\", (error: AlterError) => {\n if (options.onError) {\n options.onError(error);\n }\n this.close();\n }),\n );\n\n // Event handler (analytics)\n if (options.onEvent) {\n this._perOpenCleanups.push(\n this.eventEmitter.on(\n \"event\",\n (eventName: string, metadata: Record<string, unknown>) => {\n options.onEvent!(eventName, metadata);\n },\n ),\n );\n }\n }\n\n /**\n * Create an AlterError object\n */\n private createError(\n code: string,\n message: string,\n details?: Record<string, unknown>,\n ): Error {\n const error = new Error(message) as Error & {\n code?: string;\n details?: Record<string, unknown>;\n };\n error.code = code;\n error.details = details;\n return error;\n }\n}\n"],"names":["DEFAULT_ALTER_HOST","debugLog","config","args","debug","console","log","EventEmitter","constructor","this","events","Map","on","event","handler","has","set","Set","get","add","off","handlers","delete","emit","forEach","error","removeAllListeners","clear","StateManager","state","isOpen","sessionToken","listeners","getState","key","setState","updates","notifyListeners","subscribe","listener","clearListeners","currentState","isMobileDevice","userAgent","navigator","vendor","window","opera","isMobileUA","test","hasTouchScreen","maxTouchPoints","msMaxTouchPoints","isSmallScreen","innerWidth","getDeviceType","parseOperation","value","parseStatus","cleanupRedirectState","returnUrl","sessionStorage","removeItem","cleanUrl","URL","history","replaceState","document","title","pathname","search","hash","location","OAuthHandler","options","popup","pollInterval","messageListener","settled","baseURL","onSuccess","onError","onCancel","popupWidth","popupHeight","expectedOrigin","origin","Error","startOAuth","url","parsed","code","message","deviceType","innerHeight","shouldUseRedirectFlow","openPopup","startRedirectFlow","timestamp","Date","now","href","setItem","JSON","stringify","details","checkOAuthReturn","stateJson","getItem","parse","urlParams","URLSearchParams","success","grantId","provider","accountIdentifier","grant","grant_id","provider_name","account_identifier","toISOString","operation","scopes","split","status","alterError","err","left","screenX","outerWidth","top","screenY","outerHeight","features","join","open","startPolling","setupMessageListener","close","closed","clearInterval","removeEventListener","setInterval","data","type","rawGrants","Array","isArray","grants","length","item","push","metadata","error_description","addEventListener","VERSION","hasWarnedNonDefaultHost","AlterConnect","_oauthHandler","_perOpenCleanups","undefined","validateConfig","mergeConfig","_baseURL","replace","warn","eventEmitter","stateManager","_isInitialized","version","checkRedirectReturn","create","createError","token","handleOAuthSuccess","handleOAuthError","handleOAuthCancel","registerEventHandlers","connectURL","encodeURIComponent","onEvent","cleanup","destroy","getVersion","cleanupHandler","onExit","eventName"],"mappings":"4OAcO,MAAMA,EAAqB,yCAwHlBC,EAASC,KAAuBC,GAC1CD,EAAOE,OAITC,QAAQC,IAAI,qBAAsBH,EAEtC,OChIaI,EAGX,WAAAC,GACEC,KAAKC,OAAS,IAAIC,GACpB,CAMA,EAAAC,CAAGC,EAAeC,GAQhB,OAPKL,KAAKC,OAAOK,IAAIF,IACnBJ,KAAKC,OAAOM,IAAIH,EAAO,IAAII,KAG7BR,KAAKC,OAAOQ,IAAIL,GAAQM,IAAIL,GAGrB,IAAML,KAAKW,IAAIP,EAAOC,EAC/B,CAKA,GAAAM,CAAIP,EAAeC,GACjB,MAAMO,EAAWZ,KAAKC,OAAOQ,IAAIL,GAC7BQ,GACFA,EAASC,OAAOR,EAEpB,CAKA,IAAAS,CAAKV,KAAkBV,GACrB,MAAMkB,EAAWZ,KAAKC,OAAOQ,IAAIL,GAC7BQ,GACFA,EAASG,QAASV,IAChB,IACGA,KAA+CX,EAClD,CAAE,MAAOsB,GACPpB,QAAQoB,MACN,+CAA+CZ,MAC/CY,EAEJ,GAGN,CAKA,kBAAAC,CAAmBb,GACbA,EACFJ,KAAKC,OAAOY,OAAOT,GAEnBJ,KAAKC,OAAOiB,OAEhB,QCrDWC,EAIX,WAAApB,GACEC,KAAKoB,MAAQ,CACXC,QAAQ,EACRC,aAAc,KACdN,MAAO,MAEThB,KAAKuB,UAAY,IAAIf,GACvB,CAKA,QAAAgB,GACE,MAAO,IAAKxB,KAAKoB,MACnB,CAKA,GAAAX,CAA8BgB,GAC5B,OAAOzB,KAAKoB,MAAMK,EACpB,CAKA,QAAAC,CAASC,GACP3B,KAAKoB,MAAQ,IAAKpB,KAAKoB,SAAUO,GACjC3B,KAAK4B,iBACP,CAKA,SAAAC,CAAUC,GAIR,OAHA9B,KAAKuB,UAAUb,IAAIoB,GAGZ,KACL9B,KAAKuB,UAAUV,OAAOiB,GAE1B,CAKA,cAAAC,GACE/B,KAAKuB,UAAUL,OACjB,CAKQ,eAAAU,GACN,MAAMI,EAAehC,KAAKwB,WAC1BxB,KAAKuB,UAAUR,QAASe,IACtB,IACEA,EAASE,EACX,CAAE,MAAOhB,GACPpB,QAAQoB,MAAM,2CAA4CA,EAC5D,GAEJ,WC/EciB,IAId,MAAMC,EACJC,UAAUD,WACVC,UAAUC,QACTC,OAAuCC,OACxC,GAKIC,EADJ,iEAC6BC,KAAKN,GAG9BO,EACJ,iBAAkBJ,QAClBF,UAAUO,eAAiB,IACzBP,UACCQ,kBAAoB,GAAK,EAGxBC,EAAgBP,OAAOQ,YAAc,IAE3C,OAAON,GAAeE,GAAkBG,CAC1C,UAqBgBE,IACd,OAhBOb,KAAoBI,OAAOQ,YAAc,IAgBpB,QAR1BZ,KAAoBI,OAAOQ,WAAa,KAAOR,OAAOQ,YAAc,KASzC,SACtB,SACT,CCnCA,SAASE,EAAeC,GACtB,MAAc,WAAVA,EAA2B,SACxB,UACT,CAKA,SAASC,EAAYD,GACnB,MAAc,YAAVA,EAA4B,UAClB,UAAVA,EAA0B,QACvB,QACT,CAKA,SAASE,EAAqBC,GAC5BC,eAAeC,WAAW,qBAC1B,IACE,MAAMC,EAAW,IAAIC,IAAIJ,GACzBd,OAAOmB,QAAQC,aACb,CAAA,EACAC,SAASC,MACTL,EAASM,SAAWN,EAASO,OAASP,EAASQ,KAEnD,CAAE,MAEAzB,OAAOmB,QAAQC,aAAa,CAAA,EAAIC,SAASC,MAAOtB,OAAO0B,SAASH,SAClE,CACF,OAKaI,EAQX,WAAAjE,CAAYkE,GAPJjE,KAAAkE,MAAuB,KACvBlE,KAAAmE,aAA8B,KAC9BnE,KAAAoE,gBAA0D,KAG1DpE,KAAAqE,SAAmB,EAGzBrE,KAAKiE,QAAU,CACbK,QAASL,EAAQK,QACjBC,UAAWN,EAAQM,UACnBC,QAASP,EAAQO,QACjBC,SAAUR,EAAQQ,SAClBC,WAAYT,EAAQS,YAAc,IAClCC,YAAaV,EAAQU,aAAe,IACpChF,MAAOsE,EAAQtE,QAAS,EACxBiF,eAAgBX,EAAQW,gBAAkB,IAI5C,IACE5E,KAAK4E,eAAiB,IAAIrB,IAAIU,EAAQK,SAASO,MACjD,CAAE,MACA,MAAM,IAAIC,MACR,qBAAqBb,EAAQK,sDAAsD/E,OAEvF,CACF,CAKA,UAAAwF,CAAWC,GAGT,IAAKhF,KAAKiE,QAAQW,eAChB,IACE,MAAMK,EAAS,IAAI1B,IAAIyB,GACvBhF,KAAKiE,QAAQW,eAAiBK,EAAOJ,OACrC7E,KAAKH,IAAI,2BAA4BG,KAAKiE,QAAQW,eACpD,CAAE,MAOA,OANA5E,KAAKH,IAAI,mDAAoDmF,QAC7DhF,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,oBACNC,QACE,uEAGN,aDtCJ,MAAMC,EAAatC,IAGnB,MAAmB,UAAfsC,GAGe,WAAfA,GAA2B/C,OAAOgD,YAAchD,OAAOQ,UAM7D,CC6BQyC,IAIFtF,KAAKH,IAAI,gCACTG,KAAKuF,UAAUP,KAJfhF,KAAKH,IAAI,yCACTG,KAAKwF,kBAAkBR,GAK3B,CAMQ,iBAAAQ,CAAkBR,GACxBhF,KAAKH,IAAI,0BAA2BmF,GAGpC,MAAM5D,EAAQ,CACZqE,UAAWC,KAAKC,MAChBxC,UAAWd,OAAO0B,SAAS6B,MAG7B,IACExC,eAAeyC,QAAQ,oBAAqBC,KAAKC,UAAU3E,GAC7D,CAAE,MAAOJ,GAOP,OANAhB,KAAKH,IAAI,wBAAyBmB,QAClChB,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,iBACNC,QAAS,2DACTa,QAAS,CAAEhF,UAGf,CAGAqB,OAAO0B,SAAS6B,KAAOZ,CACzB,CAQA,uBAAOiB,CACL1B,EACAC,GAGA,MAAM0B,EAAY9C,eAAe+C,QAAQ,qBACzC,IAAKD,EACH,OAAO,EAGT,IACE,MAAM9E,EAAQ0E,KAAKM,MAAMF,GAIzB,GADgBR,KAAKC,MAAQvE,EAAMqE,UACrB,IAGZ,OADArC,eAAeC,WAAW,sBACnB,EAIT,MAAMgD,EAAY,IAAIC,gBAAgBjE,OAAO0B,SAASF,QAChD0C,EAAUF,EAAU5F,IAAI,yBACxBO,EAAQqF,EAAU5F,IAAI,uBAE5B,GAAgB,SAAZ8F,EAAoB,CAEtB,MAAMC,EAAUH,EAAU5F,IAAI,YACxBgG,EAAWJ,EAAU5F,IAAI,YACzBiG,EAAoBL,EAAU5F,IAAI,sBAExC,IAAK+F,IAAYC,IAAaC,EAM5B,OALAxD,EAAqB9B,EAAM+B,WAC3BqB,EAAQ,CACNU,KAAM,mBACNC,QAAS,mDAEJ,EAGT,MAAMwB,EAAe,CACnBC,SAAUJ,EACVC,SAAUA,EACVI,cAAeR,EAAU5F,IAAI,kBAAoBgG,EACjDK,mBAAoBJ,EACpBjB,UAAWY,EAAU5F,IAAI,eAAgB,IAAIiF,MAAOqB,cACpDC,UAAWjE,EAAesD,EAAU5F,IAAI,cACxCwG,OAAQZ,EAAU5F,IAAI,WAAWyG,MAAM,MAAQ,GAC/CC,OAAQlE,EAAYoD,EAAU5F,IAAI,YAKpC,OAFAyC,EAAqB9B,EAAM+B,WAC3BoB,EAAU,CAACoC,KACJ,CACT,CAEA,GAAI3F,EAAO,CACT,MAAMoG,EAAyB,CAC7BlC,KAAMmB,EAAU5F,IAAI,eAAiB,cACrC0E,QACEkB,EAAU5F,IAAI,sBAAwB,8BAK1C,OAFAyC,EAAqB9B,EAAM+B,WAC3BqB,EAAQ4C,IACD,CACT,CAEA,OAAO,CACT,CAAE,MAAOC,GAGP,OAFAzH,QAAQoB,MAAM,gDAAiDqG,GAC/DjE,eAAeC,WAAW,sBACnB,CACT,CACF,CAKA,SAAAkC,CAAUP,GAER,MAAMsC,EACJjF,OAAOkF,SAAWlF,OAAOmF,WAAaxH,KAAKiE,QAAQS,YAAc,EAC7D+C,EACJpF,OAAOqF,SAAWrF,OAAOsF,YAAc3H,KAAKiE,QAAQU,aAAe,EAE/DiD,EAAW,CACf,SAAS5H,KAAKiE,QAAQS,aACtB,UAAU1E,KAAKiE,QAAQU,cACvB,QAAQ2C,IACR,OAAOG,IACP,gBACA,iBACA,cACAI,KAAK,KAEP7H,KAAKH,IAAI,uBAAwBmF,GAGjChF,KAAKkE,MAAQ7B,OAAOyF,KAAK9C,EAAK,oBAAqB4C,GAE9C5H,KAAKkE,OAUVlE,KAAK+H,eACL/H,KAAKgI,wBAVHhI,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,gBACNC,QACE,oEAQR,CAKA,KAAA8C,GACEjI,KAAKH,IAAI,yBAGLG,KAAKkE,QAAUlE,KAAKkE,MAAMgE,QAC5BlI,KAAKkE,MAAM+D,QAEbjI,KAAKkE,MAAQ,KAGa,OAAtBlE,KAAKmE,eACPgE,cAAcnI,KAAKmE,cACnBnE,KAAKmE,aAAe,MAIlBnE,KAAKoE,kBACP/B,OAAO+F,oBAAoB,UAAWpI,KAAKoE,iBAC3CpE,KAAKoE,gBAAkB,KAE3B,CAKQ,YAAA2D,GACN/H,KAAKmE,aAAe9B,OAAOgG,YAAY,KACjCrI,KAAKqE,SACJrE,KAAKkE,QAASlE,KAAKkE,MAAMgE,SAC5BlI,KAAKH,IAAI,wBACTG,KAAKqE,SAAU,EACfrE,KAAKiI,QACLjI,KAAKiE,QAAQQ,aAEd,IACL,CAKQ,oBAAAuD,GACNhI,KAAKoE,gBAAmBhE,IAEtB,GAAIJ,KAAKqE,QAAS,OAGlB,GAAIjE,EAAMyE,SAAW7E,KAAK4E,eAOxB,YANA5E,KAAKH,IACH,2CACAO,EAAMyE,OACN,aACA7E,KAAK4E,eAAiB,KAK1B5E,KAAKH,IAAI,wBAAyBO,EAAMyE,QAExC,MAAMyD,EAAOlI,EAAMkI,KAGnB,GAAIA,GAAwB,iBAATA,EACjB,GAAkB,0BAAdA,EAAKC,KAAkC,CACzCvI,KAAKH,IAAI,iBAGT,MAAM2I,EAAYC,MAAMC,QAAQJ,EAAKK,QAAUL,EAAKK,OAAS,KAC7D,GAAIH,EAAW,CACbxI,KAAKH,IAAI,0BAA2B2I,EAAUI,OAAQ,UACtD,MAAMD,EAAkB,GACxB,IAAK,MAAME,KAAQL,EAAW,CAC5B,MAAMhC,EAAUqC,EAAKjC,SAChBJ,GAAYqC,EAAKpC,SAItBkC,EAAOG,KAAK,CACVlC,SAAUJ,EACVC,SAAUoC,EAAKpC,SACfI,cACGgC,EAAKhC,eAA6BgC,EAAKpC,SAC1CK,mBAAqB+B,EAAK/B,oBAAiC,GAC3DrB,UACGoD,EAAKpD,YAAwB,IAAIC,MAAOqB,cAC3CC,UAAWjE,EAAe8F,EAAK7B,WAC/BC,OAAQwB,MAAMC,QAAQG,EAAK5B,QACtB4B,EAAK5B,OACN,GACJE,OAAQlE,EAAY4F,EAAK1B,QACzB4B,SAAUF,EAAKE,WAhBf/I,KAAKH,IAAI,+BAAgCgJ,EAkB7C,CAEA,OAAsB,IAAlBF,EAAOC,QACT5I,KAAKqE,SAAU,EACfrE,KAAKiI,aACLjI,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,mBACNC,QAAS,yCAKbnF,KAAKqE,SAAU,EACfrE,KAAKiI,aACLjI,KAAKiE,QAAQM,UAAUoE,GAEzB,CAGA3I,KAAKqE,SAAU,EACfrE,KAAKiI,QACLjI,KAAKiE,QAAQO,QAAQ,CACnBU,KAAM,mBACNC,QAAS,gDAEb,MAAO,GAAkB,wBAAdmD,EAAKC,KAAgC,CAC9CvI,KAAKH,IAAI,eAET,MAAMmB,EAAoB,CACxBkE,KAAOoD,EAAKtH,OAAoB,cAChCmE,QACGmD,EAAKU,mBACN,6BACFhD,QAASsC,GAGXtI,KAAKqE,SAAU,EACfrE,KAAKiI,QACLjI,KAAKiE,QAAQO,QAAQxD,EACvB,GAIJqB,OAAO4G,iBAAiB,UAAWjJ,KAAKoE,gBAC1C,CAKQ,GAAAvE,IAAOH,GACTM,KAAKiE,QAAQtE,OAIfC,QAAQC,IAAI,qBAAsBH,EAEtC,ECzYF,MAAMwJ,EAAU,QAOhB,IAAIC,GAA0B,QAKjBC,EAYX,WAAArJ,CAAoBN,EAA6B,IAU/C,GAjBMO,KAAAqJ,cAAqC,KAErCrJ,KAAAsJ,iBAAsC,GLX1C,SAAyB7J,GAC7B,QAAuB8J,IAAnB9J,EAAO6E,QAYT,MARwB7E,EAAO6E,QAQzB,IAAIQ,MACR,iHAuEN,CKnEI0E,CAAe/J,GACfO,KAAKP,OLuEH,SAAsBA,GAC1B,MAAO,CACLE,MAAOF,EAAOE,QAAS,EAE3B,CK3EkB8J,CAAYhK,GAM1BO,KAAK0J,UAAYjK,EAAO6E,SAAW/E,GAAoBoK,QAAQ,OAAQ,IACnE3J,KAAK0J,WAAanK,IAAuB4J,EAAyB,CACpEA,GAA0B,EAO1B,MAAMtE,EAAS,IAAItB,IAAIvD,KAAK0J,UAAU7E,OACtCjF,QAAQgK,KACN,iDAAiD/E,4DAGrD,CAGA7E,KAAK6J,aAAe,IAAI/J,EACxBE,KAAK8J,aAAe,IAAI3I,EACxBnB,KAAK+J,gBAAiB,EAEtBvK,EAASQ,KAAKP,OAAQ,gCAAiC,CACrDuK,QAASd,EACT5E,QAAStE,KAAK0J,WAIhB1J,KAAKiK,qBACP,CAMQ,mBAAAA,GACUjG,EAAaiC,iBAC1B0C,IACCnJ,EAASQ,KAAKP,OAAQ,mCAAoCkJ,GAE1D3I,KAAK6J,aAAa/I,KAAK,UAAW6H,IAEnC3H,IACCxB,EAASQ,KAAKP,OAAQ,iCAAkCuB,GAExDhB,KAAK6J,aAAa/I,KAAK,QAASE,MAKlCxB,EAASQ,KAAKP,OAAQ,6CAE1B,CAYA,aAAOyK,CAAOzK,GACZ,OAAO,IAAI2J,EAAa3J,EAC1B,CA0BA,UAAMqI,CAAK7D,GAET,IAAKjE,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,wDAOJ,GAHA3K,EAASQ,KAAKP,OAAQ,uBAGjBwE,EAAQmG,OAAkC,iBAAlBnG,EAAQmG,MACnC,MAAMpK,KAAKmK,YACT,kBACA,iGAIJ,IAAKlG,EAAQM,WAA0C,mBAAtBN,EAAQM,UACvC,MAAMvE,KAAKmK,YACT,kBACA,kCAKJ,GAAInK,KAAKqB,SAEP,YADA7B,EAASQ,KAAKP,OAAQ,8BAMxBO,KAAKqJ,cAAgB,IAAIrF,EAAa,CACpCM,QAAStE,KAAK0J,SACdnF,UAAYoE,IACVnJ,EAASQ,KAAKP,OAAQ,iBAAkBkJ,GACxC3I,KAAKqK,mBAAmB1B,IAE1BnE,QAAUxD,IACRxB,EAASQ,KAAKP,OAAQ,eAAgBuB,GACtChB,KAAKsK,iBAAiBtJ,IAExByD,SAAU,KACRjF,EAASQ,KAAKP,OAAQ,kCACtBO,KAAKuK,qBAEP7F,WAAY,IACZC,YAAa,IACbhF,MAAOK,KAAKP,OAAOE,QAIrBK,KAAK8J,aAAapI,SAAS,CACzBL,QAAQ,EACRL,MAAO,KACPM,aAAc2C,EAAQmG,QAIxBpK,KAAKwK,sBAAsBvG,GAG3B,MAAMwG,EAAa,GAAGzK,KAAK0J,sCAAsCgB,mBAAmBzG,EAAQmG,SAE5F5K,EAASQ,KAAKP,OAAQ,eAAgBgL,GAGtCzK,KAAKqJ,cAActE,WAAW0F,GAG1BxG,EAAQ0G,SACV1G,EAAQ0G,QAAQ,iBAAkB,CAChClF,WAAW,IAAIC,MAAOqB,gBAI1BvH,EAASQ,KAAKP,OAAQ,iCACxB,CAKA,KAAAwI,GAEE,IAAKjI,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,yDAIJ3K,EAASQ,KAAKP,OAAQ,sBAGlBO,KAAKqJ,gBACPrJ,KAAKqJ,cAAcpB,QACnBjI,KAAKqJ,cAAgB,MAIvBrJ,KAAKsJ,iBAAiBvI,QAAS6J,GAAYA,KAC3C5K,KAAKsJ,iBAAmB,GAGxBtJ,KAAK8J,aAAapI,SAAS,CAAEL,QAAQ,IAErCrB,KAAK6J,aAAa/I,KAAK,QACzB,CAKA,OAAA+J,GACO7K,KAAK+J,iBAKVvK,EAASQ,KAAKP,OAAQ,2BAGlBO,KAAKqJ,gBACPrJ,KAAKqJ,cAAcpB,QACnBjI,KAAKqJ,cAAgB,MAInBrJ,KAAK8J,aAAarJ,IAAI,WACxBT,KAAK8J,aAAapI,SAAS,CAAEL,QAAQ,IAIvCrB,KAAK6J,aAAa5I,qBAClBjB,KAAK8J,aAAa/H,iBAClB/B,KAAK+J,gBAAiB,EACxB,CAKA,EAAA5J,CAAGC,EAAeC,GAEhB,IAAKL,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,sDAIJ,OAAOnK,KAAK6J,aAAa1J,GAAGC,EAAOC,EACrC,CAKA,GAAAM,CAAIP,EAAeC,GAEjB,IAAKL,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,uDAIJnK,KAAK6J,aAAalJ,IAAIP,EAAOC,EAC/B,CAKA,MAAAgB,GAEE,IAAKrB,KAAK+J,eACR,MAAM/J,KAAKmK,YACT,gBACA,0DAIJ,OAAOnK,KAAK8J,aAAarJ,IAAI,SAC/B,CAKA,UAAAqK,GACE,OAAO5B,CACT,CAOQ,cAAA6B,GACN/K,KAAKqJ,cAAgB,IACvB,CAKQ,kBAAAgB,CAAmB1B,GACzB3I,KAAK+K,iBACL/K,KAAK6J,aAAa/I,KAAK,UAAW6H,EACpC,CAKQ,gBAAA2B,CAAiBtJ,GACvBhB,KAAK+K,iBACL/K,KAAK8J,aAAapI,SAAS,CAAEV,UAC7BhB,KAAK6J,aAAa/I,KAAK,QAASE,EAClC,CAKQ,iBAAAuJ,GACNvK,KAAK+K,iBACL/K,KAAK6J,aAAa/I,KAAK,OACzB,CAKQ,qBAAA0J,CAAsBvG,GAExBA,EAAQM,WACVvE,KAAKsJ,iBAAiBR,KACpB9I,KAAK6J,aAAa1J,GAAG,UAAYwI,IAC/B1E,EAAQM,UAAUoE,GAClB3I,KAAKiI,WAMPhE,EAAQ+G,QACVhL,KAAKsJ,iBAAiBR,KACpB9I,KAAK6J,aAAa1J,GAAG,OAAQ,KAC3B8D,EAAQ+G,SACRhL,KAAKiI,WAMXjI,KAAKsJ,iBAAiBR,KACpB9I,KAAK6J,aAAa1J,GAAG,QAAUa,IACzBiD,EAAQO,SACVP,EAAQO,QAAQxD,GAElBhB,KAAKiI,WAKLhE,EAAQ0G,SACV3K,KAAKsJ,iBAAiBR,KACpB9I,KAAK6J,aAAa1J,GAChB,QACA,CAAC8K,EAAmBlC,KAClB9E,EAAQ0G,QAASM,EAAWlC,KAKtC,CAKQ,WAAAoB,CACNjF,EACAC,EACAa,GAEA,MAAMhF,EAAQ,IAAI8D,MAAMK,GAMxB,OAFAnE,EAAMkE,KAAOA,EACblE,EAAMgF,QAAUA,EACThF,CACT"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AlterConnectConfig, OpenOptions } from
|
|
1
|
+
import type { AlterConnectConfig, OpenOptions } from "../types";
|
|
2
|
+
import { type EventHandler } from "./events";
|
|
2
3
|
export declare class AlterConnect {
|
|
3
4
|
private config;
|
|
4
5
|
private eventEmitter;
|
|
@@ -13,8 +14,8 @@ export declare class AlterConnect {
|
|
|
13
14
|
open(options: OpenOptions): Promise<void>;
|
|
14
15
|
close(): void;
|
|
15
16
|
destroy(): void;
|
|
16
|
-
on(event: string, handler:
|
|
17
|
-
off(event: string, handler:
|
|
17
|
+
on(event: string, handler: EventHandler): () => void;
|
|
18
|
+
off(event: string, handler: EventHandler): void;
|
|
18
19
|
isOpen(): boolean;
|
|
19
20
|
getVersion(): string;
|
|
20
21
|
private cleanupHandler;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"alter-connect.d.ts","sourceRoot":"","sources":["../../../src/core/alter-connect.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"alter-connect.d.ts","sourceRoot":"","sources":["../../../src/core/alter-connect.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,kBAAkB,EAClB,WAAW,EAGZ,MAAM,UAAU,CAAC;AAQlB,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAmB3D,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,gBAAgB,CAAyB;IAKjD,OAAO;IA2CP,OAAO,CAAC,mBAAmB;IA6B3B,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,YAAY;IA4BlD,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAoF/C,KAAK,IAAI,IAAI;IA8Bb,OAAO,IAAI,IAAI;IA4Bf,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,IAAI;IAepD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI;IAe/C,MAAM,IAAI,OAAO;IAejB,UAAU,IAAI,MAAM;IASpB,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,qBAAqB;IA+C7B,OAAO,CAAC,WAAW;CAapB"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { AlterConnectConfig } from
|
|
1
|
+
import type { AlterConnectConfig } from "../types";
|
|
2
2
|
export declare const DEFAULT_ALTER_HOST = "https://backend.alterauth.com";
|
|
3
3
|
interface FullConfig {
|
|
4
4
|
debug: boolean;
|
|
5
5
|
}
|
|
6
6
|
export declare function validateConfig(config: AlterConnectConfig): void;
|
|
7
7
|
export declare function mergeConfig(config: AlterConnectConfig): FullConfig;
|
|
8
|
-
export declare function debugLog(config: FullConfig, ...args:
|
|
8
|
+
export declare function debugLog(config: FullConfig, ...args: unknown[]): void;
|
|
9
9
|
export type { FullConfig };
|
|
10
10
|
//# sourceMappingURL=config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/core/config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAUnD,eAAO,MAAM,kBAAkB,kCAAkC,CAAC;AAKlE,UAAU,UAAU;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAcD,wBAAgB,cAAc,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/core/config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAUnD,eAAO,MAAM,kBAAkB,kCAAkC,CAAC;AAKlE,UAAU,UAAU;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAcD,wBAAgB,cAAc,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAqF/D;AAKD,wBAAgB,WAAW,CAAC,MAAM,EAAE,kBAAkB,GAAG,UAAU,CAIlE;AAKD,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAOrE;AAKD,YAAY,EAAE,UAAU,EAAE,CAAC"}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
type EventHandler = (...args:
|
|
1
|
+
export type EventHandler = (...args: never[]) => void;
|
|
2
2
|
export declare class EventEmitter {
|
|
3
3
|
private events;
|
|
4
4
|
constructor();
|
|
5
5
|
on(event: string, handler: EventHandler): () => void;
|
|
6
6
|
off(event: string, handler: EventHandler): void;
|
|
7
|
-
emit(event: string, ...args:
|
|
7
|
+
emit(event: string, ...args: unknown[]): void;
|
|
8
8
|
removeAllListeners(event?: string): void;
|
|
9
9
|
}
|
|
10
|
-
export {};
|
|
11
10
|
//# sourceMappingURL=events.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../src/core/events.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../src/core/events.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,IAAI,CAAC;AAKtD,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAiC;;IAU/C,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,IAAI;IAcpD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI;IAU/C,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAmB7C,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;CAOzC"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { AlterConnect as default } from
|
|
2
|
-
export type { AlterConnectConfig, OpenOptions, Provider, Grant, AlterError, } from
|
|
1
|
+
export { AlterConnect as default } from "./core/alter-connect";
|
|
2
|
+
export type { AlterConnectConfig, OpenOptions, Provider, Grant, AlterError, } from "./types";
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/oauth/handler.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAIlD,UAAU,mBAAmB;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,IAAI,CAAC;IACrC,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/oauth/handler.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAIlD,UAAU,mBAAmB;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,IAAI,CAAC;IACrC,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAwCD,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,eAAe,CAAgD;IACvE,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,OAAO,CAAkB;gBAErB,OAAO,EAAE,mBAAmB;IAyBxC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAgC7B,OAAO,CAAC,iBAAiB;IA+BzB,MAAM,CAAC,gBAAgB,CACrB,SAAS,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,IAAI,EACpC,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GACnC,OAAO;IA6EV,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAuC5B,KAAK,IAAI,IAAI;IAyBb,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,oBAAoB;IAoG5B,OAAO,CAAC,GAAG;CAQZ"}
|
|
@@ -11,19 +11,19 @@ export interface Grant {
|
|
|
11
11
|
provider_name: string;
|
|
12
12
|
account_identifier: string;
|
|
13
13
|
timestamp: string;
|
|
14
|
-
operation:
|
|
14
|
+
operation: "creation" | "reauth";
|
|
15
15
|
scopes: string[];
|
|
16
|
-
status:
|
|
16
|
+
status: "active" | "pending" | "error";
|
|
17
17
|
metadata?: {
|
|
18
18
|
account_display_name?: string;
|
|
19
19
|
account_email?: string;
|
|
20
|
-
[key: string]:
|
|
20
|
+
[key: string]: unknown;
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
export interface AlterError {
|
|
24
24
|
code: string;
|
|
25
25
|
message: string;
|
|
26
|
-
details?: Record<string,
|
|
26
|
+
details?: Record<string, unknown>;
|
|
27
27
|
}
|
|
28
28
|
export interface AlterConnectConfig {
|
|
29
29
|
debug?: boolean;
|
|
@@ -34,6 +34,6 @@ export interface OpenOptions {
|
|
|
34
34
|
onSuccess: (grants: Grant[]) => void;
|
|
35
35
|
onExit?: () => void;
|
|
36
36
|
onError?: (error: AlterError) => void;
|
|
37
|
-
onEvent?: (eventName: string, metadata:
|
|
37
|
+
onEvent?: (eventName: string, metadata: Record<string, unknown>) => void;
|
|
38
38
|
}
|
|
39
39
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAKD,MAAM,WAAW,KAAK;IAKpB,QAAQ,EAAE,MAAM,CAAC;IAMjB,QAAQ,EAAE,MAAM,CAAC;IAMjB,aAAa,EAAE,MAAM,CAAC;IAMtB,kBAAkB,EAAE,MAAM,CAAC;IAK3B,SAAS,EAAE,MAAM,CAAC;IAOlB,SAAS,EAAE,UAAU,GAAG,QAAQ,CAAC;IAMjC,MAAM,EAAE,MAAM,EAAE,CAAC;IAKjB,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;IAKvC,QAAQ,CAAC,EAAE;QACT,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,CAAC,GAAG,EAAE,MAAM,GAAG,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAKD,MAAM,WAAW,KAAK;IAKpB,QAAQ,EAAE,MAAM,CAAC;IAMjB,QAAQ,EAAE,MAAM,CAAC;IAMjB,aAAa,EAAE,MAAM,CAAC;IAMtB,kBAAkB,EAAE,MAAM,CAAC;IAK3B,SAAS,EAAE,MAAM,CAAC;IAOlB,SAAS,EAAE,UAAU,GAAG,QAAQ,CAAC;IAMjC,MAAM,EAAE,MAAM,EAAE,CAAC;IAKjB,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;IAKvC,QAAQ,CAAC,EAAE;QACT,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH;AAMD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAKD,MAAM,WAAW,kBAAkB;IAIjC,KAAK,CAAC,EAAE,OAAO,CAAC;IAWhB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAKD,MAAM,WAAW,WAAW;IAK1B,KAAK,EAAE,MAAM,CAAC;IAMd,SAAS,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,IAAI,CAAC;IAKrC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAKpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAMtC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC1E"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare function isMobileDevice(): boolean;
|
|
2
2
|
export declare function isPhoneDevice(): boolean;
|
|
3
3
|
export declare function isTabletDevice(): boolean;
|
|
4
|
-
export declare function getDeviceType():
|
|
4
|
+
export declare function getDeviceType(): "desktop" | "tablet" | "phone";
|
|
5
5
|
export declare function shouldUseRedirectFlow(): boolean;
|
|
6
6
|
//# sourceMappingURL=mobile.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile.d.ts","sourceRoot":"","sources":["../../../src/utils/mobile.ts"],"names":[],"mappings":"AAOA,wBAAgB,cAAc,IAAI,OAAO,
|
|
1
|
+
{"version":3,"file":"mobile.d.ts","sourceRoot":"","sources":["../../../src/utils/mobile.ts"],"names":[],"mappings":"AAOA,wBAAgB,cAAc,IAAI,OAAO,CA0BxC;AAKD,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAKD,wBAAgB,cAAc,IAAI,OAAO,CAIxC;AAKD,wBAAgB,aAAa,IAAI,SAAS,GAAG,QAAQ,GAAG,OAAO,CAI9D;AAUD,wBAAgB,qBAAqB,IAAI,OAAO,CAa/C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alter-ai/connect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "JavaScript SDK for Alter Connect - Secure OAuth integration UI",
|
|
5
5
|
"main": "dist/alter-connect.cjs.js",
|
|
6
6
|
"module": "dist/alter-connect.esm.js",
|
|
@@ -51,19 +51,22 @@
|
|
|
51
51
|
"node": ">=20.19.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
+
"@eslint/js": "^9.39.4",
|
|
54
55
|
"@rollup/plugin-commonjs": "^28.0.0",
|
|
55
56
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
56
57
|
"@rollup/plugin-terser": "^1.0.0",
|
|
57
58
|
"@rollup/plugin-typescript": "^11.1.5",
|
|
58
59
|
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
59
60
|
"@typescript-eslint/parser": "^8.53.0",
|
|
60
|
-
"@vitest/ui": "^
|
|
61
|
-
"eslint": "^9.
|
|
61
|
+
"@vitest/ui": "^4.1.8",
|
|
62
|
+
"eslint": "^9.39.4",
|
|
63
|
+
"eslint-plugin-no-unsanitized": "^4.1.5",
|
|
64
|
+
"eslint-plugin-security": "^4.0.1",
|
|
62
65
|
"jsdom": "^29.1.1",
|
|
63
66
|
"rollup": "^4.60.4",
|
|
64
67
|
"tslib": "^2.6.2",
|
|
65
68
|
"typescript": "^5.3.3",
|
|
66
69
|
"typescript-eslint": "^8.53.0",
|
|
67
|
-
"vitest": "^
|
|
70
|
+
"vitest": "^4.1.8"
|
|
68
71
|
}
|
|
69
72
|
}
|