botschat 0.1.10 → 0.1.13
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/README.md +11 -15
- package/migrations/0012_push_tokens.sql +11 -0
- package/package.json +20 -1
- package/packages/api/src/do/connection-do.ts +142 -24
- package/packages/api/src/env.ts +6 -0
- package/packages/api/src/index.ts +7 -0
- package/packages/api/src/routes/auth.ts +85 -9
- package/packages/api/src/routes/channels.ts +3 -2
- package/packages/api/src/routes/dev-auth.ts +45 -0
- package/packages/api/src/routes/push.ts +52 -0
- package/packages/api/src/routes/upload.ts +73 -38
- package/packages/api/src/utils/fcm.ts +167 -0
- package/packages/api/src/utils/firebase.ts +218 -0
- package/packages/plugin/dist/src/channel.d.ts +6 -0
- package/packages/plugin/dist/src/channel.d.ts.map +1 -1
- package/packages/plugin/dist/src/channel.js +71 -15
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/package.json +1 -1
- package/packages/web/dist/assets/index-B9qN5gs6.js +1 -0
- package/packages/web/dist/assets/index-BQNMGVyU.js +2 -0
- package/packages/web/dist/assets/{index-Ev5M8VmV.css → index-Bd_RDcgO.css} +1 -1
- package/packages/web/dist/assets/index-Civeg2lm.js +1 -0
- package/packages/web/dist/assets/index-Dk33VSnY.js +2 -0
- package/packages/web/dist/assets/index-Kr85Nj_-.js +1516 -0
- package/packages/web/dist/assets/index-lVB82JKU.js +1 -0
- package/packages/web/dist/assets/index.esm-CtMkqqqb.js +599 -0
- package/packages/web/dist/assets/web-CUXjh_UA.js +1 -0
- package/packages/web/dist/assets/web-vKLTVUul.js +1 -0
- package/packages/web/dist/index.html +6 -4
- package/packages/web/dist/sw.js +158 -1
- package/packages/web/index.html +4 -2
- package/packages/web/package.json +4 -1
- package/packages/web/src/App.tsx +117 -1
- package/packages/web/src/api.ts +21 -1
- package/packages/web/src/components/AccountSettings.tsx +131 -0
- package/packages/web/src/components/ChatWindow.tsx +302 -70
- package/packages/web/src/components/CronSidebar.tsx +89 -24
- package/packages/web/src/components/DataConsentModal.tsx +249 -0
- package/packages/web/src/components/LoginPage.tsx +55 -7
- package/packages/web/src/components/MessageContent.tsx +71 -9
- package/packages/web/src/components/MobileLayout.tsx +28 -118
- package/packages/web/src/components/SessionTabs.tsx +41 -2
- package/packages/web/src/components/Sidebar.tsx +88 -66
- package/packages/web/src/e2e.ts +26 -5
- package/packages/web/src/firebase.ts +215 -3
- package/packages/web/src/foreground.ts +51 -0
- package/packages/web/src/index.css +10 -2
- package/packages/web/src/main.tsx +24 -2
- package/packages/web/src/push.ts +205 -0
- package/packages/web/src/ws.ts +20 -8
- package/scripts/dev.sh +158 -26
- package/scripts/mock-openclaw.mjs +382 -0
- package/scripts/test-e2e-chat.ts +2 -2
- package/scripts/test-e2e-live.ts +1 -1
- package/wrangler.toml +3 -0
- package/packages/web/dist/assets/index-DpW6VzZK.js +0 -1497
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{W as j}from"./index-Kr85Nj_-.js";class T extends j{constructor(){super()}parseJwt(e){const t=e.split(".")[1].replace(/-/g,"+").replace(/_/g,"/"),r=decodeURIComponent(atob(t).split("").map(n=>"%"+("00"+n.charCodeAt(0).toString(16)).slice(-2)).join(""));return JSON.parse(r)}async loadScript(e){return new Promise((o,t)=>{const r=document.createElement("script");r.src=e,r.async=!0,r.onload=()=>{o()},r.onerror=t,document.body.appendChild(r)})}}T.OAUTH_STATE_KEY="social_login_oauth_pending";class B extends T{constructor(){super(...arguments),this.clientId=null,this.redirectUrl=null,this.scriptLoaded=!1,this.scriptUrl="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js",this.useProperTokenExchange=!1}async initialize(e,o,t=!1){this.clientId=e,this.redirectUrl=o||null,this.useProperTokenExchange=t,e&&await this.loadAppleScript()}async login(e){if(!this.clientId)throw new Error("Apple Client ID not set. Call initialize() first.");if(!this.scriptLoaded)throw new Error("Apple Sign-In script not loaded.");return new Promise((o,t)=>{var r,n;AppleID.auth.init({clientId:(r=this.clientId)!==null&&r!==void 0?r:"",scope:((n=e.scopes)===null||n===void 0?void 0:n.join(" "))||"name email",redirectURI:this.redirectUrl||window.location.href,state:e.state,nonce:e.nonce,usePopup:!0}),AppleID.auth.signIn().then(i=>{var a,s,l,c,h;let u=null;this.useProperTokenExchange?u=null:u={token:i.authorization.code||""};const p=Object.assign({profile:{user:i.user||"",email:((a=i.user)===null||a===void 0?void 0:a.email)||null,givenName:((l=(s=i.user)===null||s===void 0?void 0:s.name)===null||l===void 0?void 0:l.firstName)||null,familyName:((h=(c=i.user)===null||c===void 0?void 0:c.name)===null||h===void 0?void 0:h.lastName)||null},accessToken:u,idToken:i.authorization.id_token||null},this.useProperTokenExchange&&{authorizationCode:i.authorization.code});o({provider:"apple",result:p})}).catch(i=>{t(i)})})}async logout(){console.log("Apple logout: Session should be managed on the client side")}async isLoggedIn(){return console.log("Apple login status should be managed on the client side"),{isLoggedIn:!1}}async getAuthorizationCode(){throw console.log("Apple authorization code should be stored during login"),new Error("Apple authorization code not available")}async refresh(){console.log("Apple refresh not available on web")}async loadAppleScript(){if(!this.scriptLoaded)return this.loadScript(this.scriptUrl).then(()=>{this.scriptLoaded=!0})}}class Y extends T{constructor(){super(...arguments),this.appId=null,this.scriptLoaded=!1,this.locale="en_US"}async initialize(e,o){this.appId=e,o&&(this.locale=o),e&&(await this.loadFacebookScript(this.locale),FB.init({appId:this.appId,version:"v17.0",xfbml:!0,cookie:!0}))}async login(e){if(!this.appId)throw new Error("Facebook App ID not set. Call initialize() first.");return new Promise((o,t)=>{FB.login(r=>{r.status==="connected"?FB.api("/me",{fields:"id,name,email,picture"},n=>{var i,a;const s={accessToken:{token:r.authResponse.accessToken,userId:r.authResponse.userID},profile:{userID:n.id,name:n.name,email:n.email||null,imageURL:((a=(i=n.picture)===null||i===void 0?void 0:i.data)===null||a===void 0?void 0:a.url)||null,friendIDs:[],birthday:null,ageRange:null,gender:null,location:null,hometown:null,profileURL:null},idToken:null};o({provider:"facebook",result:s})}):t(new Error("Facebook login failed"))},{scope:e.permissions.join(",")})})}async logout(){return new Promise(e=>{FB.logout(()=>e())})}async isLoggedIn(){return new Promise(e=>{FB.getLoginStatus(o=>{e({isLoggedIn:o.status==="connected"})})})}async getAuthorizationCode(){return new Promise((e,o)=>{FB.getLoginStatus(t=>{var r;t.status==="connected"?e({accessToken:((r=t.authResponse)===null||r===void 0?void 0:r.accessToken)||""}):o(new Error("No Facebook authorization code available"))})})}async refresh(e){await this.login(e)}async loadFacebookScript(e){if(this.scriptLoaded)return;const o=document.querySelector('script[src*="connect.facebook.net"]');return o&&o.remove(),this.loadScript(`https://connect.facebook.net/${e}/sdk.js`).then(()=>{this.scriptLoaded=!0})}}class V extends T{constructor(){super(...arguments),this.clientId=null,this.loginType="online",this.GOOGLE_TOKEN_REQUEST_URL="https://www.googleapis.com/oauth2/v3/tokeninfo",this.GOOGLE_STATE_KEY="capgo_social_login_google_state"}async initialize(e,o,t,r){this.clientId=e,o&&(this.loginType=o),this.hostedDomain=t,this.redirectUrl=r}async login(e){if(!this.clientId)throw new Error("Google Client ID not set. Call initialize() first.");let o=e.scopes||[];o.length>0?(o.includes("https://www.googleapis.com/auth/userinfo.email")||o.push("https://www.googleapis.com/auth/userinfo.email"),o.includes("https://www.googleapis.com/auth/userinfo.profile")||o.push("https://www.googleapis.com/auth/userinfo.profile"),o.includes("openid")||o.push("openid")):o=["https://www.googleapis.com/auth/userinfo.email","https://www.googleapis.com/auth/userinfo.profile","openid"];const t=e.nonce||Math.random().toString(36).substring(2);return this.traditionalOAuth({scopes:o,nonce:t,hostedDomain:this.hostedDomain,prompt:e.prompt})}async logout(){if(this.loginType==="offline")return Promise.reject("Offline login doesn't store tokens. logout is not available");const e=this.getGoogleState();e&&await this.rawLogoutGoogle(e.accessToken)}async isLoggedIn(){if(this.loginType==="offline")return Promise.reject("Offline login doesn't store tokens. isLoggedIn is not available");const e=this.getGoogleState();if(!e)return{isLoggedIn:!1};try{const o=await this.accessTokenIsValid(e.accessToken),t=this.idTokenValid(e.idToken);if(o&&t)return{isLoggedIn:!0};try{await this.rawLogoutGoogle(e.accessToken,!1)}catch(r){console.error("Access token is not valid, but cannot logout",r)}return{isLoggedIn:!1}}catch(o){return Promise.reject(o)}}async getAuthorizationCode(){if(this.loginType==="offline")return Promise.reject("Offline login doesn't store tokens. getAuthorizationCode is not available");const e=this.getGoogleState();if(!e)throw new Error("No Google authorization code available");try{const o=await this.accessTokenIsValid(e.accessToken),t=this.idTokenValid(e.idToken);if(o&&t)return{accessToken:e.accessToken,jwt:e.idToken};try{await this.rawLogoutGoogle(e.accessToken,!1)}catch(r){console.error("Access token is not valid, but cannot logout",r)}throw new Error("No Google authorization code available")}catch(o){return Promise.reject(o)}}async refresh(){return Promise.reject("Not implemented")}handleOAuthRedirect(e){const o=e.searchParams,t=o.get("error");if(t)return localStorage.removeItem(T.OAUTH_STATE_KEY),{error:o.get("error_description")||t};const r=o.get("code");if(r&&o.has("scope"))return{provider:"google",result:{serverAuthCode:r,responseType:"offline"}};const n=e.hash.substring(1);if(console.log("handleOAuthRedirect",e.hash),!n)return null;const i=new URLSearchParams(n),a=i.get("error");if(a)return localStorage.removeItem(T.OAUTH_STATE_KEY),{error:i.get("error_description")||a};console.log("handleOAuthRedirect ok");const s=i.get("access_token"),l=i.get("id_token");if(s&&l){localStorage.removeItem(T.OAUTH_STATE_KEY);const c=this.parseJwt(l);return{provider:"google",result:{accessToken:{token:s},idToken:l,profile:{email:c.email||null,familyName:c.family_name||null,givenName:c.given_name||null,id:c.sub||null,name:c.name||null,imageUrl:c.picture||null},responseType:"online"}}}return null}async accessTokenIsValid(e){const o=`${this.GOOGLE_TOKEN_REQUEST_URL}?access_token=${encodeURIComponent(e)}`;try{const t=await fetch(o);if(!t.ok)return console.log(`Invalid response from ${this.GOOGLE_TOKEN_REQUEST_URL}. Response not successful. Status code: ${t.status}. Assuming that the token is not valid`),!1;const r=await t.text();if(!r)throw console.error(`Invalid response from ${this.GOOGLE_TOKEN_REQUEST_URL}. Response body is null`),new Error(`Invalid response from ${this.GOOGLE_TOKEN_REQUEST_URL}. Response body is null`);let n;try{n=JSON.parse(r)}catch(s){throw console.error(`Invalid response from ${this.GOOGLE_TOKEN_REQUEST_URL}. Response body is not valid JSON. Error: ${s}`),new Error(`Invalid response from ${this.GOOGLE_TOKEN_REQUEST_URL}. Response body is not valid JSON. Error: ${s}`)}const i=n.expires_in;if(i==null)throw console.error(`Invalid response from ${this.GOOGLE_TOKEN_REQUEST_URL}. Response JSON does not include 'expires_in'.`),new Error(`Invalid response from ${this.GOOGLE_TOKEN_REQUEST_URL}. Response JSON does not include 'expires_in'.`);let a;try{if(a=parseInt(i,10),isNaN(a))throw new Error("'expires_in' is not a valid integer")}catch(s){throw console.error(`Invalid response from ${this.GOOGLE_TOKEN_REQUEST_URL}. 'expires_in': ${i} is not a valid integer. Error: ${s}`),new Error(`Invalid response from ${this.GOOGLE_TOKEN_REQUEST_URL}. 'expires_in': ${i} is not a valid integer. Error: ${s}`)}return a>5}catch(t){throw console.error(t),t}}idTokenValid(e){try{const o=this.parseJwt(e),t=Math.ceil(Date.now()/1e3)+5;return o.exp&&t<o.exp}catch{return!1}}async rawLogoutGoogle(e,o=null){if(o===null&&(o=await this.accessTokenIsValid(e)),o===!0){try{await fetch(`https://accounts.google.com/o/oauth2/revoke?token=${encodeURIComponent(e)}`),this.clearStateGoogle()}catch{}return}else{this.clearStateGoogle();return}}persistStateGoogle(e,o){try{window.localStorage.setItem(this.GOOGLE_STATE_KEY,JSON.stringify({accessToken:e,idToken:o}))}catch(t){console.error("Cannot persist state google",t)}}clearStateGoogle(){try{window.localStorage.removeItem(this.GOOGLE_STATE_KEY)}catch(e){console.error("Cannot clear state google",e)}}getGoogleState(){try{const e=window.localStorage.getItem(this.GOOGLE_STATE_KEY);if(!e)return null;const{accessToken:o,idToken:t}=JSON.parse(e);return{accessToken:o,idToken:t}}catch(e){return console.error("Cannot get state google",e),null}}async traditionalOAuth({scopes:e,hostedDomain:o,nonce:t,prompt:r}){var n;const i=[...new Set([...e||[],"openid"])],a=new URLSearchParams(Object.assign(Object.assign({client_id:(n=this.clientId)!==null&&n!==void 0?n:"",redirect_uri:this.redirectUrl||window.location.origin+window.location.pathname,response_type:this.loginType==="offline"?"code":"token id_token",scope:i.join(" ")},t&&{nonce:t}),{include_granted_scopes:"true",state:"popup"}));o!==void 0&&a.append("hd",o),r!==void 0&&a.append("prompt",r);const s=`https://accounts.google.com/o/oauth2/v2/auth?${a.toString()}`,l=500,c=600,h=window.screenX+(window.outerWidth-l)/2,u=window.screenY+(window.outerHeight-c)/2;localStorage.setItem(T.OAUTH_STATE_KEY,JSON.stringify({provider:"google",loginType:this.loginType,nonce:t}));const p=window.open(s,"Google Sign In",`width=${l},height=${c},left=${h},top=${u},popup=1`);let y,d;const O=`google_oauth_${t||Date.now()}`;let v=null;try{v=new BroadcastChannel(O)}catch{}return new Promise((I,S)=>{if(!p){S(new Error("Failed to open popup"));return}const U=()=>{window.removeEventListener("message",R),clearInterval(y),clearTimeout(d),v&&v.close()},P=_=>{if(this.loginType==="online"){const{accessToken:E,idToken:w}=_;if(E&&w){const m=this.parseJwt(w);this.persistStateGoogle(E.token,w),I({provider:"google",result:{accessToken:{token:E.token},idToken:w,profile:{email:m.email||null,familyName:m.family_name||null,givenName:m.given_name||null,id:m.sub||null,name:m.name||null,imageUrl:m.picture||null},responseType:"online"}})}else S(new Error("Invalid OAuth response: missing accessToken or idToken"))}else{const{serverAuthCode:E}=_;if(!E){S(new Error("Invalid OAuth response: missing serverAuthCode"));return}I({provider:"google",result:{responseType:"offline",serverAuthCode:E}})}},R=_=>{var E,w,m,b;if(!(_.origin!==window.location.origin||!((w=(E=_.data)===null||E===void 0?void 0:E.source)===null||w===void 0)&&w.startsWith("angular"))){if(((m=_.data)===null||m===void 0?void 0:m.type)==="oauth-response")U(),P(_.data);else if(((b=_.data)===null||b===void 0?void 0:b.type)==="oauth-error"){U();const A=_.data.error||"User cancelled the OAuth flow";S(new Error(A))}}};v&&(v.onmessage=_=>{var E;const w=_.data;if(!(!((E=w==null?void 0:w.source)===null||E===void 0)&&E.toString().startsWith("angular"))){if((w==null?void 0:w.type)==="oauth-response")U(),P(w);else if((w==null?void 0:w.type)==="oauth-error"){U();const m=w.error||"User cancelled the OAuth flow";S(new Error(m))}}}),window.addEventListener("message",R),d=setTimeout(()=>{U();try{p.close()}catch{}S(new Error("OAuth timeout"))},3e5),y=setInterval(()=>{try{p.closed&&(U(),S(new Error("Popup closed")))}catch{clearInterval(y)}},1e3)})}}var F=function(k,e){var o={};for(var t in k)Object.prototype.hasOwnProperty.call(k,t)&&e.indexOf(t)<0&&(o[t]=k[t]);if(k!=null&&typeof Object.getOwnPropertySymbols=="function")for(var r=0,t=Object.getOwnPropertySymbols(k);r<t.length;r++)e.indexOf(t[r])<0&&Object.prototype.propertyIsEnumerable.call(k,t[r])&&(o[t[r]]=k[t[r]]);return o};class J extends T{constructor(){super(...arguments),this.providers=new Map,this.TOKENS_KEY_PREFIX="capgo_social_login_oauth2_tokens_",this.STATE_PREFIX="capgo_social_login_oauth2_state_"}normalizeScopeValue(e){return e?typeof e=="string"?e:Array.isArray(e)?e.filter(Boolean).join(" "):"":""}normalizeConfig(e,o){var t,r,n,i,a,s,l,c;const h=(t=o.appId)!==null&&t!==void 0?t:o.clientId,u=(r=o.authorizationBaseUrl)!==null&&r!==void 0?r:o.authorizationEndpoint,p=(n=o.accessTokenEndpoint)!==null&&n!==void 0?n:o.tokenEndpoint,y=(i=o.logoutUrl)!==null&&i!==void 0?i:o.endSessionEndpoint,d=(a=o.scope)!==null&&a!==void 0?a:o.scopes;if(!h)throw new Error(`OAuth2 provider '${e}' requires appId (or clientId).`);if(!o.redirectUrl)throw new Error(`OAuth2 provider '${e}' requires redirectUrl.`);if(!u&&!o.issuerUrl)throw new Error(`OAuth2 provider '${e}' requires authorizationBaseUrl (or authorizationEndpoint) or issuerUrl.`);return{appId:h,issuerUrl:o.issuerUrl,authorizationBaseUrl:u,accessTokenEndpoint:p,redirectUrl:o.redirectUrl,resourceUrl:o.resourceUrl,responseType:(s=o.responseType)!==null&&s!==void 0?s:"code",pkceEnabled:(l=o.pkceEnabled)!==null&&l!==void 0?l:!0,scope:this.normalizeScopeValue(d),additionalParameters:o.additionalParameters,loginHint:o.loginHint,prompt:o.prompt,additionalTokenParameters:o.additionalTokenParameters,additionalResourceHeaders:o.additionalResourceHeaders,logoutUrl:y,postLogoutRedirectUrl:o.postLogoutRedirectUrl,additionalLogoutParameters:o.additionalLogoutParameters,logsEnabled:(c=o.logsEnabled)!==null&&c!==void 0?c:!1}}async ensureDiscovered(e){const o=this.providers.get(e);if(!(o!=null&&o.issuerUrl)||o.authorizationBaseUrl&&o.accessTokenEndpoint)return;const r=`${o.issuerUrl.replace(/\/+$/,"")}/.well-known/openid-configuration`,n=await fetch(r);if(!n.ok){const c=await n.text().catch(()=>"");throw new Error(`OAuth2 discovery failed (${n.status}): ${c||r}`)}const i=await n.json(),a=i.authorization_endpoint,s=i.token_endpoint,l=i.end_session_endpoint;!o.authorizationBaseUrl&&typeof a=="string"&&(o.authorizationBaseUrl=a),!o.accessTokenEndpoint&&typeof s=="string"&&(o.accessTokenEndpoint=s),!o.logoutUrl&&typeof l=="string"&&(o.logoutUrl=l),o.logsEnabled&&console.log(`[OAuth2:${e}] Discovery resolved`,{authorizationBaseUrl:o.authorizationBaseUrl,accessTokenEndpoint:o.accessTokenEndpoint,logoutUrl:o.logoutUrl})}async initializeProviders(e){for(const[o,t]of Object.entries(e)){const r=this.normalizeConfig(o,t);this.providers.set(o,r),r.logsEnabled&&console.log(`[OAuth2:${o}] Initialized with config:`,{appId:r.appId,issuerUrl:r.issuerUrl,authorizationBaseUrl:r.authorizationBaseUrl,redirectUrl:r.redirectUrl,responseType:r.responseType,pkceEnabled:r.pkceEnabled}),await this.ensureDiscovered(o)}}getProvider(e){const o=this.providers.get(e);if(!o)throw new Error(`OAuth2 provider '${e}' not configured. Call initialize() first.`);return o}getTokensKey(e){return`${this.TOKENS_KEY_PREFIX}${e}`}async login(e){var o,t,r,n,i,a,s,l,c;const{providerId:h}=e,u=this.getProvider(h);await this.ensureDiscovered(h);const p=(o=e.redirectUrl)!==null&&o!==void 0?o:u.redirectUrl,y=this.normalizeScopeValue((r=(t=e.scope)!==null&&t!==void 0?t:e.scopes)!==null&&r!==void 0?r:u.scope),d=(n=e.state)!==null&&n!==void 0?n:this.generateState(),O=(i=e.codeVerifier)!==null&&i!==void 0?i:this.generateCodeVerifier(),v=new URLSearchParams({response_type:u.responseType,client_id:u.appId,redirect_uri:p,state:d});y&&v.set("scope",y);const I=Object.assign(Object.assign({},(a=u.additionalParameters)!==null&&a!==void 0?a:{}),(s=e.additionalParameters)!==null&&s!==void 0?s:{}),S=(l=e.loginHint)!==null&&l!==void 0?l:u.loginHint,U=(c=e.prompt)!==null&&c!==void 0?c:u.prompt;if(S&&!("login_hint"in I)&&(I.login_hint=S),U&&!("prompt"in I)&&(I.prompt=U),u.responseType==="code"&&u.pkceEnabled){const b=await this.generateCodeChallenge(O);v.set("code_challenge",b),v.set("code_challenge_method","S256")}for(const[b,A]of Object.entries(I))A!==void 0&&v.set(b,A);if(this.persistPendingLogin(d,{providerId:h,codeVerifier:O,redirectUri:p,scope:y}),localStorage.setItem(T.OAUTH_STATE_KEY,JSON.stringify({provider:"oauth2",providerId:h,state:d})),!u.authorizationBaseUrl)throw new Error(`OAuth2 provider '${h}' is missing authorizationBaseUrl (discovery may have failed).`);const P=`${u.authorizationBaseUrl}?${v.toString()}`;if(u.logsEnabled&&console.log(`[OAuth2:${h}] Opening authorization URL:`,P),e.flow==="redirect")return window.location.assign(P),new Promise(()=>{});const R=500,_=650,E=window.screenX+(window.outerWidth-R)/2,w=window.screenY+(window.outerHeight-_)/2,m=window.open(P,"OAuth2Login",`width=${R},height=${_},left=${E},top=${w},popup=1`);return new Promise((b,A)=>{if(!m){A(new Error("Unable to open login window. Please allow popups."));return}const f=`oauth2_${d}`;let L=null;try{L=new BroadcastChannel(f)}catch{u.logsEnabled&&console.log(`[OAuth2:${h}] BroadcastChannel not supported, using postMessage only`)}const x=(g,D,G)=>{window.removeEventListener("message",g),clearTimeout(D),clearInterval(G),L&&L.close()},K=g=>{if((g==null?void 0:g.type)==="oauth-response"){if(g!=null&&g.provider&&g.provider!=="oauth2"||g!=null&&g.providerId&&g.providerId!==h)return!1;x($,N,z);const D=g,{provider:G,type:X}=D,H=F(D,["provider","type"]);return b({provider:"oauth2",result:H}),!0}else if((g==null?void 0:g.type)==="oauth-error")return g!=null&&g.provider&&g.provider!=="oauth2"?!1:(x($,N,z),A(new Error(g.error||"OAuth2 login was cancelled.")),!0);return!1};L&&(L.onmessage=g=>{K(g.data)});const $=g=>{g.origin===window.location.origin&&K(g.data)};window.addEventListener("message",$);const N=window.setTimeout(()=>{x($,N,z);try{m.close()}catch{}A(new Error("OAuth2 login timed out."))},3e5),z=window.setInterval(()=>{try{m.closed&&(x($,N,z),A(new Error("OAuth2 login window was closed.")))}catch{clearInterval(z),u.logsEnabled&&console.log(`[OAuth2:${h}] Cannot check popup.closed due to cross-origin restrictions. Relying on message handlers and timeout.`)}},1e3)})}async logout(e){await this.ensureDiscovered(e);const o=this.providers.get(e),t=this.getStoredTokens(e);if(localStorage.removeItem(this.getTokensKey(e)),o!=null&&o.logoutUrl)try{const r=new URL(o.logoutUrl);t!=null&&t.idToken&&r.searchParams.set("id_token_hint",t.idToken);const n=o.postLogoutRedirectUrl;if(n&&r.searchParams.set("post_logout_redirect_uri",n),o.additionalLogoutParameters)for(const[i,a]of Object.entries(o.additionalLogoutParameters))r.searchParams.set(i,a);window.open(r.toString(),"_blank")}catch{window.open(o.logoutUrl,"_blank")}}async isLoggedIn(e){const o=this.getStoredTokens(e);if(!o)return{isLoggedIn:!1};const t=o.expiresAt>Date.now();return t||localStorage.removeItem(this.getTokensKey(e)),{isLoggedIn:t}}async getAuthorizationCode(e){const o=this.getStoredTokens(e);if(!o)throw new Error(`OAuth2 access token is not available for provider '${e}'.`);return{accessToken:o.accessToken,jwt:o.idToken}}async refresh(e){await this.refreshToken(e)}async refreshToken(e,o,t){var r,n,i,a,s,l;await this.ensureDiscovered(e);const c=this.getProvider(e),h=this.getStoredTokens(e),u=o??(h==null?void 0:h.refreshToken);if(!u)throw new Error(`No OAuth2 refresh token is available for provider '${e}'. Include offline_access scope to receive one.`);if(!c.accessTokenEndpoint)throw new Error(`No accessTokenEndpoint configured for provider '${e}'.`);const p=await this.refreshWithRefreshToken(e,u,t),y=p.expires_in?Date.now()+p.expires_in*1e3:Date.now()+36e5,d=(i=(n=(r=p.scope)===null||r===void 0?void 0:r.split(" ").filter(Boolean))!==null&&n!==void 0?n:h==null?void 0:h.scope)!==null&&i!==void 0?i:[];let O=null;c.resourceUrl&&(O=await this.fetchResource(e,p.access_token));const v=(a=p.refresh_token)!==null&&a!==void 0?a:u;return this.persistTokens(e,{accessToken:p.access_token,refreshToken:v,idToken:p.id_token,expiresAt:y,scope:d,tokenType:p.token_type}),{providerId:e,accessToken:{token:p.access_token,tokenType:p.token_type,expires:new Date(y).toISOString(),refreshToken:v},idToken:(s=p.id_token)!==null&&s!==void 0?s:null,refreshToken:v??null,resourceData:O,scope:d,tokenType:p.token_type,expiresIn:(l=p.expires_in)!==null&&l!==void 0?l:null}}async handleOAuthRedirect(e,o){var t,r,n,i,a;const s=new URLSearchParams(e.search);new URLSearchParams(e.hash.slice(1)).forEach((d,O)=>{s.set(O,d)});const c=o??s.get("state");if(!c)return null;const h=this.consumePendingLogin(c);if(!h)return localStorage.removeItem(T.OAUTH_STATE_KEY),{error:"OAuth2 login session expired or state mismatch."};const{providerId:u}=h;await this.ensureDiscovered(u);const p=this.providers.get(u);if(!p)return localStorage.removeItem(T.OAUTH_STATE_KEY),{error:`OAuth2 provider '${u}' configuration not found.`};const y=s.get("error");if(y)return localStorage.removeItem(T.OAUTH_STATE_KEY),{error:s.get("error_description")||y};try{let d;if(s.has("code")){const S=s.get("code");if(!S)return localStorage.removeItem(T.OAUTH_STATE_KEY),{error:"OAuth2 authorization code missing from redirect."};d=await this.exchangeAuthorizationCode(u,S,h)}else if(s.has("access_token"))d={access_token:s.get("access_token"),token_type:s.get("token_type")||"bearer",expires_in:s.has("expires_in")?parseInt(s.get("expires_in"),10):void 0,scope:s.get("scope")||void 0,id_token:s.get("id_token")||void 0};else return localStorage.removeItem(T.OAUTH_STATE_KEY),{error:"No authorization code or access token in redirect."};const O=d.expires_in?Date.now()+d.expires_in*1e3:Date.now()+36e5,v=(r=(t=d.scope)===null||t===void 0?void 0:t.split(" ").filter(Boolean))!==null&&r!==void 0?r:[];let I=null;return p.resourceUrl&&(I=await this.fetchResource(u,d.access_token)),this.persistTokens(u,{accessToken:d.access_token,refreshToken:d.refresh_token,idToken:d.id_token,expiresAt:O,scope:v,tokenType:d.token_type}),{provider:"oauth2",result:{providerId:u,accessToken:{token:d.access_token,tokenType:d.token_type,expires:new Date(O).toISOString(),refreshToken:d.refresh_token},idToken:(n=d.id_token)!==null&&n!==void 0?n:null,refreshToken:(i=d.refresh_token)!==null&&i!==void 0?i:null,resourceData:I,scope:v,tokenType:d.token_type,expiresIn:(a=d.expires_in)!==null&&a!==void 0?a:null}}}catch(d){return d instanceof Error?{error:d.message}:{error:"OAuth2 login failed unexpectedly."}}finally{localStorage.removeItem(T.OAUTH_STATE_KEY)}}async exchangeAuthorizationCode(e,o,t){const r=this.getProvider(e);if(!r.accessTokenEndpoint)throw new Error(`No accessTokenEndpoint configured for provider '${e}'.`);const n=new URLSearchParams({grant_type:"authorization_code",client_id:r.appId,code:o,redirect_uri:t.redirectUri});if(r.pkceEnabled&&n.set("code_verifier",t.codeVerifier),r.additionalTokenParameters)for(const[a,s]of Object.entries(r.additionalTokenParameters))n.set(a,s);r.logsEnabled&&console.log(`[OAuth2:${e}] Exchanging code at:`,r.accessTokenEndpoint);const i=await fetch(r.accessTokenEndpoint,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:n.toString()});if(!i.ok){const a=await i.text();throw new Error(`OAuth2 token exchange failed (${i.status}): ${a}`)}return await i.json()}async refreshWithRefreshToken(e,o,t){const r=this.getProvider(e);if(!r.accessTokenEndpoint)throw new Error(`No accessTokenEndpoint configured for provider '${e}'.`);const n=new URLSearchParams({grant_type:"refresh_token",refresh_token:o,client_id:r.appId});if(r.additionalTokenParameters)for(const[a,s]of Object.entries(r.additionalTokenParameters))n.set(a,s);if(t)for(const[a,s]of Object.entries(t))n.set(a,s);const i=await fetch(r.accessTokenEndpoint,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:n.toString()});if(!i.ok){const a=await i.text();throw new Error(`OAuth2 refresh failed (${i.status}): ${a}`)}return await i.json()}async fetchResource(e,o){const t=this.getProvider(e);if(!t.resourceUrl)throw new Error(`No resourceUrl configured for provider '${e}'.`);const r={Authorization:`Bearer ${o}`};t.additionalResourceHeaders&&Object.assign(r,t.additionalResourceHeaders);const n=await fetch(t.resourceUrl,{headers:r});if(!n.ok){const i=await n.text();throw new Error(`Unable to fetch OAuth2 resource (${n.status}): ${i}`)}return await n.json()}persistTokens(e,o){localStorage.setItem(this.getTokensKey(e),JSON.stringify(o))}getStoredTokens(e){const o=localStorage.getItem(this.getTokensKey(e));if(!o)return null;try{return JSON.parse(o)}catch(t){return console.warn(`Failed to parse stored OAuth2 tokens for provider '${e}'`,t),null}}persistPendingLogin(e,o){localStorage.setItem(`${this.STATE_PREFIX}${e}`,JSON.stringify(o))}consumePendingLogin(e){const o=`${this.STATE_PREFIX}${e}`,t=localStorage.getItem(o);if(localStorage.removeItem(o),!t)return null;try{return JSON.parse(t)}catch(r){return console.warn("Failed to parse pending OAuth2 login payload",r),null}}generateState(){return[...crypto.getRandomValues(new Uint8Array(16))].map(e=>e.toString(16).padStart(2,"0")).join("")}generateCodeVerifier(){const e=new Uint8Array(64);return crypto.getRandomValues(e),Array.from(e).map(o=>"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"[o%66]).join("")}async generateCodeChallenge(e){const t=new TextEncoder().encode(e),r=await crypto.subtle.digest("SHA-256",t);return this.base64UrlEncode(new Uint8Array(r))}base64UrlEncode(e){let o="";return e.forEach(t=>o+=String.fromCharCode(t)),btoa(o).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}decodeIdToken(e){const o=e.split(".");if(o.length<2)throw new Error("Invalid JWT: missing parts");const r=o[1].replace(/-/g,"+").replace(/_/g,"/"),n=r+"=".repeat((4-r.length%4)%4),i=atob(n);return JSON.parse(i)}getAccessTokenExpirationDate(e){const o=this.getStoredTokens(e);return o!=null&&o.expiresAt?{expirationDate:new Date(o.expiresAt).toISOString()}:{expirationDate:null}}isAccessTokenAvailable(e){const o=this.getStoredTokens(e);return{isAvailable:!!(o!=null&&o.accessToken)}}isAccessTokenExpired(e){const o=this.getStoredTokens(e);return o!=null&&o.expiresAt?{isExpired:o.expiresAt<=Date.now()}:{isExpired:!0}}isRefreshTokenAvailable(e){const o=this.getStoredTokens(e);return{isAvailable:!!(o!=null&&o.refreshToken)}}}var W=function(k,e){var o={};for(var t in k)Object.prototype.hasOwnProperty.call(k,t)&&e.indexOf(t)<0&&(o[t]=k[t]);if(k!=null&&typeof Object.getOwnPropertySymbols=="function")for(var r=0,t=Object.getOwnPropertySymbols(k);r<t.length;r++)e.indexOf(t[r])<0&&Object.prototype.propertyIsEnumerable.call(k,t[r])&&(o[t[r]]=k[t[r]]);return o};class M extends T{constructor(){super(...arguments),this.clientId=null,this.redirectUrl=null,this.defaultScopes=["tweet.read","users.read"],this.forceLogin=!1,this.TOKENS_KEY="capgo_social_login_twitter_tokens_v1",this.STATE_PREFIX="capgo_social_login_twitter_state_"}async initialize(e,o,t,r,n){this.clientId=e,this.redirectUrl=o??null,t!=null&&t.length&&(this.defaultScopes=t),this.forceLogin=r??!1,this.audience=n??void 0}async login(e){var o,t,r,n,i,a;if(!this.clientId)throw new Error("Twitter Client ID not configured. Call initialize() first.");const s=(t=(o=e.redirectUrl)!==null&&o!==void 0?o:this.redirectUrl)!==null&&t!==void 0?t:window.location.origin+window.location.pathname,l=!((r=e.scopes)===null||r===void 0)&&r.length?e.scopes:this.defaultScopes,c=(n=e.state)!==null&&n!==void 0?n:this.generateState(),h=(i=e.codeVerifier)!==null&&i!==void 0?i:this.generateCodeVerifier(),u=await this.generateCodeChallenge(h);this.persistPendingLogin(c,{codeVerifier:h,redirectUri:s,scopes:l}),localStorage.setItem(T.OAUTH_STATE_KEY,JSON.stringify({provider:"twitter",state:c}));const p=new URLSearchParams({response_type:"code",client_id:this.clientId,redirect_uri:s,scope:l.join(" "),state:c,code_challenge:u,code_challenge_method:"S256"});((a=e.forceLogin)!==null&&a!==void 0?a:this.forceLogin)===!0&&p.set("force_login","true"),this.audience&&p.set("audience",this.audience);const y=`https://x.com/i/oauth2/authorize?${p.toString()}`,d=500,O=650,v=window.screenX+(window.outerWidth-d)/2,I=window.screenY+(window.outerHeight-O)/2,S=window.open(y,"XLogin",`width=${d},height=${O},left=${v},top=${I},popup=1`);return new Promise((U,P)=>{if(!S){P(new Error("Unable to open login window. Please allow popups."));return}const R=`twitter_oauth_${c}`;let _=null;try{_=new BroadcastChannel(R)}catch{}const E=(f,L,x)=>{window.removeEventListener("message",f),clearTimeout(L),clearInterval(x),_&&_.close()},w=f=>{if((f==null?void 0:f.type)==="oauth-response"){if(f!=null&&f.provider&&f.provider!=="twitter")return!1;E(m,b,A);const L=f,{provider:x,type:K}=L,$=W(L,["provider","type"]);return U({provider:"twitter",result:$}),!0}else if((f==null?void 0:f.type)==="oauth-error")return f!=null&&f.provider&&f.provider!=="twitter"?!1:(E(m,b,A),P(new Error(f.error||"Twitter login was cancelled.")),!0);return!1};_&&(_.onmessage=f=>{w(f.data)});const m=f=>{f.origin===window.location.origin&&w(f.data)};window.addEventListener("message",m);const b=window.setTimeout(()=>{E(m,b,A);try{S.close()}catch{}P(new Error("Twitter login timed out."))},3e5),A=window.setInterval(()=>{try{S.closed&&(E(m,b,A),P(new Error("Twitter login window was closed.")))}catch{clearInterval(A)}},1e3)})}async logout(){localStorage.removeItem(this.TOKENS_KEY)}async isLoggedIn(){const e=this.getStoredTokens();if(!e)return{isLoggedIn:!1};const o=e.expiresAt>Date.now();return o||localStorage.removeItem(this.TOKENS_KEY),{isLoggedIn:o}}async getAuthorizationCode(){const e=this.getStoredTokens();if(!e)throw new Error("Twitter access token is not available.");return{accessToken:e.accessToken}}async refresh(){const e=this.getStoredTokens();if(!(e!=null&&e.refreshToken))throw new Error("No Twitter refresh token is available. Include offline.access scope to receive one.");await this.refreshWithRefreshToken(e.refreshToken)}async handleOAuthRedirect(e,o){const t=e.searchParams,r=o??t.get("state");if(!r)return null;const n=this.consumePendingLogin(r);if(!n)return localStorage.removeItem(T.OAUTH_STATE_KEY),{error:"Twitter login session expired or state mismatch."};const i=t.get("error");if(i)return localStorage.removeItem(T.OAUTH_STATE_KEY),{error:t.get("error_description")||i};const a=t.get("code");if(!a)return localStorage.removeItem(T.OAUTH_STATE_KEY),{error:"Twitter authorization code missing from redirect."};try{const s=await this.exchangeAuthorizationCode(a,n),l=await this.fetchProfile(s.access_token),c=Date.now()+s.expires_in*1e3,h=s.scope.split(" ").filter(Boolean);return this.persistTokens({accessToken:s.access_token,refreshToken:s.refresh_token,expiresAt:c,scope:h,tokenType:s.token_type,userId:l.id,profile:l}),{provider:"twitter",result:{accessToken:{token:s.access_token,tokenType:s.token_type,expires:new Date(c).toISOString(),userId:l.id},refreshToken:s.refresh_token,scope:h,tokenType:s.token_type,expiresIn:s.expires_in,profile:l}}}catch(s){return s instanceof Error?{error:s.message}:{error:"Twitter login failed unexpectedly."}}finally{localStorage.removeItem(T.OAUTH_STATE_KEY)}}async exchangeAuthorizationCode(e,o){var t;const r=new URLSearchParams({grant_type:"authorization_code",client_id:(t=this.clientId)!==null&&t!==void 0?t:"",code:e,redirect_uri:o.redirectUri,code_verifier:o.codeVerifier}),n=await fetch("https://api.x.com/2/oauth2/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:r.toString()});if(!n.ok){const i=await n.text();throw new Error(`Twitter token exchange failed (${n.status}): ${i}`)}return await n.json()}async refreshWithRefreshToken(e){var o,t;const r=new URLSearchParams({grant_type:"refresh_token",refresh_token:e,client_id:(o=this.clientId)!==null&&o!==void 0?o:""}),n=await fetch("https://api.x.com/2/oauth2/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:r.toString()});if(!n.ok){const c=await n.text();throw new Error(`Twitter refresh failed (${n.status}): ${c}`)}const i=await n.json(),a=await this.fetchProfile(i.access_token),s=Date.now()+i.expires_in*1e3,l=i.scope.split(" ").filter(Boolean);this.persistTokens({accessToken:i.access_token,refreshToken:(t=i.refresh_token)!==null&&t!==void 0?t:e,expiresAt:s,scope:l,tokenType:i.token_type,userId:a.id,profile:a})}async fetchProfile(e){var o,t,r,n;const a=await fetch(`https://api.x.com/2/users/me?user.fields=${["profile_image_url","verified","name","username"].join(",")}`,{headers:{Authorization:`Bearer ${e}`}});if(!a.ok){const l=await a.text();throw new Error(`Unable to fetch Twitter profile (${a.status}): ${l}`)}const s=await a.json();if(!s.data)throw new Error("Twitter profile payload is missing data.");return{id:s.data.id,username:s.data.username,name:(o=s.data.name)!==null&&o!==void 0?o:null,profileImageUrl:(t=s.data.profile_image_url)!==null&&t!==void 0?t:null,verified:(r=s.data.verified)!==null&&r!==void 0?r:!1,email:(n=s.data.email)!==null&&n!==void 0?n:null}}persistTokens(e){localStorage.setItem(this.TOKENS_KEY,JSON.stringify(e))}getStoredTokens(){const e=localStorage.getItem(this.TOKENS_KEY);if(!e)return null;try{return JSON.parse(e)}catch(o){return console.warn("Failed to parse stored Twitter tokens",o),null}}persistPendingLogin(e,o){localStorage.setItem(`${this.STATE_PREFIX}${e}`,JSON.stringify(o))}consumePendingLogin(e){const o=`${this.STATE_PREFIX}${e}`,t=localStorage.getItem(o);if(localStorage.removeItem(o),!t)return null;try{return JSON.parse(t)}catch(r){return console.warn("Failed to parse pending Twitter login payload",r),null}}generateState(){return[...crypto.getRandomValues(new Uint8Array(16))].map(e=>e.toString(16).padStart(2,"0")).join("")}generateCodeVerifier(){const e=new Uint8Array(64);return crypto.getRandomValues(e),Array.from(e).map(o=>"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"[o%66]).join("")}async generateCodeChallenge(e){const t=new TextEncoder().encode(e),r=await crypto.subtle.digest("SHA-256",t);return this.base64UrlEncode(new Uint8Array(r))}base64UrlEncode(e){let o="";return e.forEach(t=>o+=String.fromCharCode(t)),btoa(o).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}}class C extends j{constructor(){super(),this.googleProvider=new V,this.appleProvider=new B,this.facebookProvider=new Y,this.twitterProvider=new M,this.oauth2Provider=new J;const e=!!localStorage.getItem(C.OAUTH_STATE_KEY),o=!!window.opener||C.POPUP_WINDOW_NAMES.has(window.name);e&&o&&this.finishOAuthRedirectInPopup().catch(t=>{console.error("Failed to finish OAuth redirect",t);try{window.close()}catch{}})}async parseRedirectResult(){var e;const o=new URL(window.location.href),t=localStorage.getItem(C.OAUTH_STATE_KEY);let r=null,n,i;if(t)try{const s=JSON.parse(t);r=(e=s.provider)!==null&&e!==void 0?e:null,n=s.state,i=s.nonce}catch{r=t==="true"?"google":null}let a=null;switch(r){case"twitter":a=await this.twitterProvider.handleOAuthRedirect(o,n);break;case"oauth2":a=await this.oauth2Provider.handleOAuthRedirect(o,n);break;case"google":default:a=this.googleProvider.handleOAuthRedirect(o);break}return{provider:r,state:n,nonce:i,result:a}}async finishOAuthRedirectInPopup(){var e;const o=await this.parseRedirectResult(),t=o.result;if(!t)return;let r;"error"in t?r={type:"oauth-error",provider:(e=o.provider)!==null&&e!==void 0?e:null,error:t.error}:r=Object.assign({type:"oauth-response",provider:t.provider},t.result);try{window.opener&&window.opener.postMessage(r,window.location.origin)}catch{console.log("postMessage to opener failed, using BroadcastChannel")}try{let n=null;if(o.provider==="oauth2"&&o.state?n=`oauth2_${o.state}`:o.provider==="twitter"&&o.state?n=`twitter_oauth_${o.state}`:o.provider==="google"&&o.nonce&&(n=`google_oauth_${o.nonce}`),n){const i=new BroadcastChannel(n);i.postMessage(r),i.close()}}catch{console.log("BroadcastChannel not available")}window.close()}async initialize(e){var o,t,r,n;const i=[];!((o=e.google)===null||o===void 0)&&o.webClientId&&i.push(this.googleProvider.initialize(e.google.webClientId,e.google.mode,e.google.hostedDomain,e.google.redirectUrl)),!((t=e.apple)===null||t===void 0)&&t.clientId&&i.push(this.appleProvider.initialize(e.apple.clientId,e.apple.redirectUrl,e.apple.useProperTokenExchange)),!((r=e.facebook)===null||r===void 0)&&r.appId&&i.push(this.facebookProvider.initialize(e.facebook.appId,e.facebook.locale)),!((n=e.twitter)===null||n===void 0)&&n.clientId&&i.push(this.twitterProvider.initialize(e.twitter.clientId,e.twitter.redirectUrl,e.twitter.defaultScopes,e.twitter.forceLogin,e.twitter.audience)),e.oauth2&&Object.keys(e.oauth2).length>0&&i.push(this.oauth2Provider.initializeProviders(e.oauth2)),await Promise.all(i)}async login(e){switch(e.provider){case"google":return this.googleProvider.login(e.options);case"apple":return this.appleProvider.login(e.options);case"facebook":return this.facebookProvider.login(e.options);case"twitter":return this.twitterProvider.login(e.options);case"oauth2":return this.oauth2Provider.login(e.options);default:throw new Error(`Login for ${e.provider} is not implemented on web`)}}async logout(e){switch(e.provider){case"google":return this.googleProvider.logout();case"apple":return this.appleProvider.logout();case"facebook":return this.facebookProvider.logout();case"twitter":return this.twitterProvider.logout();case"oauth2":if(!e.providerId)throw new Error("providerId is required for oauth2 logout");return this.oauth2Provider.logout(e.providerId);default:throw new Error(`Logout for ${e.provider} is not implemented`)}}async isLoggedIn(e){switch(e.provider){case"google":return this.googleProvider.isLoggedIn();case"apple":return this.appleProvider.isLoggedIn();case"facebook":return this.facebookProvider.isLoggedIn();case"twitter":return this.twitterProvider.isLoggedIn();case"oauth2":if(!e.providerId)throw new Error("providerId is required for oauth2 isLoggedIn");return this.oauth2Provider.isLoggedIn(e.providerId);default:throw new Error(`isLoggedIn for ${e.provider} is not implemented`)}}async getAuthorizationCode(e){switch(e.provider){case"google":return this.googleProvider.getAuthorizationCode();case"apple":return this.appleProvider.getAuthorizationCode();case"facebook":return this.facebookProvider.getAuthorizationCode();case"twitter":return this.twitterProvider.getAuthorizationCode();case"oauth2":if(!e.providerId)throw new Error("providerId is required for oauth2 getAuthorizationCode");return this.oauth2Provider.getAuthorizationCode(e.providerId);default:throw new Error(`getAuthorizationCode for ${e.provider} is not implemented`)}}async refresh(e){switch(e.provider){case"google":return this.googleProvider.refresh();case"apple":return this.appleProvider.refresh();case"facebook":return this.facebookProvider.refresh(e.options);case"twitter":return this.twitterProvider.refresh();case"oauth2":{const o=e.options;if(!(o!=null&&o.providerId))throw new Error("providerId is required for oauth2 refresh");return this.oauth2Provider.refresh(o.providerId)}default:throw new Error(`Refresh for ${e.provider} is not implemented`)}}async providerSpecificCall(e){throw new Error(`Provider specific call for ${e.call} is not implemented`)}async refreshToken(e){if(e.provider!=="oauth2")throw new Error("refreshToken is only implemented for oauth2 on web");return this.oauth2Provider.refreshToken(e.providerId,e.refreshToken,e.additionalParameters)}async handleRedirectCallback(){const o=(await this.parseRedirectResult()).result;if(!o)return null;if("error"in o)throw new Error(o.error);return o}async decodeIdToken(e){var o;const t=(o=e==null?void 0:e.idToken)!==null&&o!==void 0?o:e==null?void 0:e.token;if(!t)throw new Error("idToken (or token) is required");return{claims:this.oauth2Provider.decodeIdToken(t)}}async getAccessTokenExpirationDate(e){if(typeof(e==null?void 0:e.accessTokenExpirationDate)!="number")throw new Error("accessTokenExpirationDate is required");return{date:new Date(e.accessTokenExpirationDate).toISOString()}}async isAccessTokenAvailable(e){var o;const t=(o=e==null?void 0:e.accessToken)!==null&&o!==void 0?o:null;return{isAvailable:typeof t=="string"&&t.length>0}}async isAccessTokenExpired(e){if(typeof(e==null?void 0:e.accessTokenExpirationDate)!="number")throw new Error("accessTokenExpirationDate is required");return{isExpired:e.accessTokenExpirationDate<=Date.now()}}async isRefreshTokenAvailable(e){var o;const t=(o=e==null?void 0:e.refreshToken)!==null&&o!==void 0?o:null;return{isAvailable:typeof t=="string"&&t.length>0}}async getPluginVersion(){return{version:"web"}}async openSecureWindow(e){const r=[["width",600],["height",550],["left",screen.width/2-300],["top",screen.height/2-275]].map(i=>i.join("=")).join(","),n=window.open(e.authEndpoint,"Authorization",r);return typeof n.focus=="function"&&n.focus(),new Promise((i,a)=>{const s=new BroadcastChannel(e.broadcastChannelName||"oauth-channel");s.addEventListener("message",l=>{l.data.startsWith(e.redirectUri)?(s.close(),i({redirectedUri:l.data})):(s.close(),a(new Error("Redirect URI does not match, expected "+e.redirectUri+" but got "+l.data)))}),setTimeout(()=>{s.close(),a(new Error("The sign-in flow timed out"))},5*6e4)})}}C.OAUTH_STATE_KEY="social_login_oauth_pending";C.POPUP_WINDOW_NAMES=new Set(["OAuth2Login","XLogin","Google Sign In","Authorization"]);export{C as SocialLoginWeb};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{W as t}from"./index-Kr85Nj_-.js";class s extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}}export{s as AppWeb};
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en"
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
6
|
+
<script>
|
|
7
|
+
(function(){var s=localStorage.getItem("botschat_theme");var t=s==="light"||s==="dark"?s:window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";document.documentElement.setAttribute("data-theme",t)})();
|
|
8
|
+
</script>
|
|
6
9
|
|
|
7
10
|
<!-- PWA manifest -->
|
|
8
11
|
<link rel="manifest" href="/manifest.json" />
|
|
9
12
|
|
|
10
|
-
<!-- Theme color — matches dark theme bg -->
|
|
11
13
|
<meta name="theme-color" content="#1A1D21" />
|
|
12
14
|
|
|
13
15
|
<!-- iOS PWA support -->
|
|
@@ -28,8 +30,8 @@
|
|
|
28
30
|
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700&family=Noto+Sans+SC:wght@400;700&display=swap" rel="stylesheet" />
|
|
29
31
|
|
|
30
32
|
<title>BotsChat</title>
|
|
31
|
-
<script type="module" crossorigin src="/assets/index-
|
|
32
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
33
|
+
<script type="module" crossorigin src="/assets/index-Kr85Nj_-.js"></script>
|
|
34
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Bd_RDcgO.css">
|
|
33
35
|
</head>
|
|
34
36
|
<body>
|
|
35
37
|
<div id="root"></div>
|
package/packages/web/dist/sw.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Minimal service worker for PWA installability.
|
|
1
|
+
// Minimal service worker for PWA installability + Push notifications.
|
|
2
2
|
// Caches the app shell on install for faster startup.
|
|
3
3
|
|
|
4
4
|
const CACHE_NAME = "botschat-v1";
|
|
@@ -38,3 +38,160 @@ self.addEventListener("fetch", (event) => {
|
|
|
38
38
|
.catch(() => caches.match(event.request))
|
|
39
39
|
);
|
|
40
40
|
});
|
|
41
|
+
|
|
42
|
+
// ---- Push Notification Handling ----
|
|
43
|
+
|
|
44
|
+
// E2E decryption helpers (inlined from e2e-crypto to work in SW context).
|
|
45
|
+
// The SW cannot access localStorage, so the E2E key is stored in IndexedDB
|
|
46
|
+
// by the main app and read here for decryption.
|
|
47
|
+
|
|
48
|
+
const IDB_NAME = "botschat-sw";
|
|
49
|
+
const IDB_STORE = "keys";
|
|
50
|
+
const IDB_KEY = "e2e_key";
|
|
51
|
+
|
|
52
|
+
/** Open the IndexedDB database. */
|
|
53
|
+
function openDB() {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const req = indexedDB.open(IDB_NAME, 1);
|
|
56
|
+
req.onupgradeneeded = () => {
|
|
57
|
+
req.result.createObjectStore(IDB_STORE);
|
|
58
|
+
};
|
|
59
|
+
req.onsuccess = () => resolve(req.result);
|
|
60
|
+
req.onerror = () => reject(req.error);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Read the cached E2E key (Uint8Array) from IndexedDB. */
|
|
65
|
+
async function getE2eKey() {
|
|
66
|
+
try {
|
|
67
|
+
const db = await openDB();
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const tx = db.transaction(IDB_STORE, "readonly");
|
|
70
|
+
const store = tx.objectStore(IDB_STORE);
|
|
71
|
+
const req = store.get(IDB_KEY);
|
|
72
|
+
req.onsuccess = () => resolve(req.result || null);
|
|
73
|
+
req.onerror = () => reject(req.error);
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** HKDF-SHA256 nonce derivation (matches e2e-crypto). */
|
|
81
|
+
async function hkdfNonce(key, contextId) {
|
|
82
|
+
const hmacKey = await crypto.subtle.importKey(
|
|
83
|
+
"raw",
|
|
84
|
+
key.buffer,
|
|
85
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
86
|
+
false,
|
|
87
|
+
["sign"]
|
|
88
|
+
);
|
|
89
|
+
const info = new TextEncoder().encode("nonce-" + contextId);
|
|
90
|
+
const input = new Uint8Array(info.length + 1);
|
|
91
|
+
input.set(info);
|
|
92
|
+
input[info.length] = 0x01;
|
|
93
|
+
const full = await crypto.subtle.sign("HMAC", hmacKey, input.buffer);
|
|
94
|
+
return new Uint8Array(full).slice(0, 16);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Decrypt ciphertext (base64) using AES-256-CTR (matches e2e-crypto). */
|
|
98
|
+
async function decryptText(keyBytes, ciphertextB64, contextId) {
|
|
99
|
+
// base64 decode
|
|
100
|
+
const binary = atob(ciphertextB64);
|
|
101
|
+
const ciphertext = new Uint8Array(binary.length);
|
|
102
|
+
for (let i = 0; i < binary.length; i++) {
|
|
103
|
+
ciphertext[i] = binary.charCodeAt(i);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const counter = await hkdfNonce(keyBytes, contextId);
|
|
107
|
+
const aesKey = await crypto.subtle.importKey(
|
|
108
|
+
"raw",
|
|
109
|
+
keyBytes.buffer,
|
|
110
|
+
{ name: "AES-CTR" },
|
|
111
|
+
false,
|
|
112
|
+
["decrypt"]
|
|
113
|
+
);
|
|
114
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
115
|
+
{ name: "AES-CTR", counter: counter.buffer, length: 128 },
|
|
116
|
+
aesKey,
|
|
117
|
+
ciphertext.buffer
|
|
118
|
+
);
|
|
119
|
+
return new TextDecoder().decode(plaintext);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
self.addEventListener("push", (event) => {
|
|
123
|
+
const promise = (async () => {
|
|
124
|
+
let data = {};
|
|
125
|
+
if (event.data) {
|
|
126
|
+
try {
|
|
127
|
+
data = event.data.json();
|
|
128
|
+
} catch {
|
|
129
|
+
data = { text: event.data.text() };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// FCM wraps the data payload; extract it
|
|
134
|
+
const payload = data.data || data;
|
|
135
|
+
const msgType = payload.type || "agent.text";
|
|
136
|
+
const isEncrypted = payload.encrypted === "1";
|
|
137
|
+
const messageId = payload.messageId || "";
|
|
138
|
+
|
|
139
|
+
let title = "BotsChat";
|
|
140
|
+
let body = "New message";
|
|
141
|
+
|
|
142
|
+
if (isEncrypted && messageId && payload.text) {
|
|
143
|
+
// Attempt client-side E2E decryption
|
|
144
|
+
const key = await getE2eKey();
|
|
145
|
+
if (key) {
|
|
146
|
+
try {
|
|
147
|
+
body = await decryptText(key, payload.text, messageId);
|
|
148
|
+
if (body.length > 200) body = body.slice(0, 200) + "\u2026";
|
|
149
|
+
} catch {
|
|
150
|
+
body = "New encrypted message";
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
body = "New encrypted message";
|
|
154
|
+
}
|
|
155
|
+
} else if (payload.text) {
|
|
156
|
+
body = payload.text;
|
|
157
|
+
if (body.length > 200) body = body.slice(0, 200) + "\u2026";
|
|
158
|
+
} else if (msgType === "agent.media") {
|
|
159
|
+
body = "Sent an image";
|
|
160
|
+
} else if (msgType === "agent.a2ui") {
|
|
161
|
+
body = "New interactive message";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const options = {
|
|
165
|
+
body,
|
|
166
|
+
icon: "/icons/icon-192.png",
|
|
167
|
+
badge: "/icons/badge-72.png",
|
|
168
|
+
tag: "botschat-message",
|
|
169
|
+
renotify: true,
|
|
170
|
+
data: payload,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return self.registration.showNotification(title, options);
|
|
174
|
+
})();
|
|
175
|
+
|
|
176
|
+
event.waitUntil(promise);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
self.addEventListener("notificationclick", (event) => {
|
|
180
|
+
event.notification.close();
|
|
181
|
+
|
|
182
|
+
event.waitUntil(
|
|
183
|
+
clients
|
|
184
|
+
.matchAll({ type: "window", includeUncontrolled: true })
|
|
185
|
+
.then((windowClients) => {
|
|
186
|
+
for (const client of windowClients) {
|
|
187
|
+
if (
|
|
188
|
+
client.url.includes("console.botschat.app") ||
|
|
189
|
+
client.url.includes("localhost")
|
|
190
|
+
) {
|
|
191
|
+
return client.focus();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return clients.openWindow("https://console.botschat.app");
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
});
|
package/packages/web/index.html
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en"
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
6
|
+
<script>
|
|
7
|
+
(function(){var s=localStorage.getItem("botschat_theme");var t=s==="light"||s==="dark"?s:window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";document.documentElement.setAttribute("data-theme",t)})();
|
|
8
|
+
</script>
|
|
6
9
|
|
|
7
10
|
<!-- PWA manifest -->
|
|
8
11
|
<link rel="manifest" href="/manifest.json" />
|
|
9
12
|
|
|
10
|
-
<!-- Theme color — matches dark theme bg -->
|
|
11
13
|
<meta name="theme-color" content="#1A1D21" />
|
|
12
14
|
|
|
13
15
|
<!-- iOS PWA support -->
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
"preview": "vite preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@capacitor/core": "^8.1.0",
|
|
13
|
+
"@capacitor/keyboard": "^8.0.0",
|
|
14
|
+
"@capacitor/status-bar": "^8.0.1",
|
|
12
15
|
"@tailwindcss/typography": "^0.5.19",
|
|
13
|
-
"firebase": "^12.9.0",
|
|
14
16
|
"e2e-crypto": "*",
|
|
17
|
+
"firebase": "^12.9.0",
|
|
15
18
|
"react": "^19.0.0",
|
|
16
19
|
"react-dom": "^19.0.0",
|
|
17
20
|
"react-markdown": "^10.1.0",
|
package/packages/web/src/App.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useReducer, useEffect, useCallback, useRef, useState } from "react";
|
|
2
|
+
import { Capacitor } from "@capacitor/core";
|
|
2
3
|
import { Group, Panel, useDefaultLayout } from "react-resizable-panels";
|
|
3
4
|
import {
|
|
4
5
|
appReducer,
|
|
@@ -12,15 +13,19 @@ import {
|
|
|
12
13
|
import { getToken, setToken, setRefreshToken, agentsApi, channelsApi, tasksApi, jobsApi, authApi, messagesApi, modelsApi, meApi, sessionsApi, type ModelInfo } from "./api";
|
|
13
14
|
import { ModelSelect } from "./components/ModelSelect";
|
|
14
15
|
import { BotsChatWSClient, type WSMessage } from "./ws";
|
|
16
|
+
import { initPushNotifications } from "./push";
|
|
17
|
+
import { setupForegroundDetection } from "./foreground";
|
|
15
18
|
import { IconRail } from "./components/IconRail";
|
|
16
19
|
import { Sidebar } from "./components/Sidebar";
|
|
17
20
|
import { ChatWindow } from "./components/ChatWindow";
|
|
18
21
|
import { ThreadPanel } from "./components/ThreadPanel";
|
|
19
22
|
import { JobList } from "./components/JobList";
|
|
20
23
|
import { LoginPage } from "./components/LoginPage";
|
|
24
|
+
import { DataConsentModal } from "./components/DataConsentModal";
|
|
21
25
|
import { OnboardingPage } from "./components/OnboardingPage";
|
|
22
26
|
import { ConnectionSettings } from "./components/ConnectionSettings";
|
|
23
27
|
import { E2ESettings } from "./components/E2ESettings";
|
|
28
|
+
import { AccountSettings } from "./components/AccountSettings";
|
|
24
29
|
import { DebugLogPanel } from "./components/DebugLogPanel";
|
|
25
30
|
import { CronSidebar } from "./components/CronSidebar";
|
|
26
31
|
import { CronDetail } from "./components/CronDetail";
|
|
@@ -64,6 +69,15 @@ export default function App() {
|
|
|
64
69
|
const mainLayout = useDefaultLayout({ id: "botschat-main" });
|
|
65
70
|
const contentLayout = useDefaultLayout({ id: "botschat-content" });
|
|
66
71
|
|
|
72
|
+
const [dataConsentGiven, setDataConsentGiven] = useState(() => {
|
|
73
|
+
return localStorage.getItem("botschat_data_consent") === "1";
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const handleDataConsent = useCallback(() => {
|
|
77
|
+
setDataConsentGiven(true);
|
|
78
|
+
localStorage.setItem("botschat_data_consent", "1");
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
67
81
|
// Onboarding: show setup page for new users who haven't connected OpenClaw yet.
|
|
68
82
|
// Once dismissed (skip or connected), we remember it for this session.
|
|
69
83
|
const [onboardingDismissed, setOnboardingDismissed] = useState(() => {
|
|
@@ -85,9 +99,14 @@ export default function App() {
|
|
|
85
99
|
useEffect(() => {
|
|
86
100
|
document.documentElement.setAttribute("data-theme", theme);
|
|
87
101
|
localStorage.setItem("botschat_theme", theme);
|
|
88
|
-
// Sync PWA theme-color meta tag with current theme
|
|
89
102
|
const meta = document.querySelector('meta[name="theme-color"]');
|
|
90
103
|
if (meta) meta.setAttribute("content", theme === "dark" ? "#1A1D21" : "#FFFFFF");
|
|
104
|
+
// Sync native status bar style on Capacitor
|
|
105
|
+
if (Capacitor.isNativePlatform()) {
|
|
106
|
+
import("@capacitor/status-bar").then(({ StatusBar, Style }) => {
|
|
107
|
+
StatusBar.setStyle({ style: theme === "dark" ? Style.Dark : Style.Light }).catch(() => {});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
91
110
|
}, [theme]);
|
|
92
111
|
|
|
93
112
|
// Persist active view (messages / automations)
|
|
@@ -123,6 +142,54 @@ export default function App() {
|
|
|
123
142
|
|
|
124
143
|
// ---- Auto-login on mount ----
|
|
125
144
|
useEffect(() => {
|
|
145
|
+
// Dev-token auth bypass: ?dev_token=xxx in URL
|
|
146
|
+
const params = new URLSearchParams(window.location.search);
|
|
147
|
+
const devToken = params.get("dev_token");
|
|
148
|
+
const devUser = params.get("dev_user");
|
|
149
|
+
if (devToken) {
|
|
150
|
+
dlog.info("Auth", "Dev-token login attempt");
|
|
151
|
+
fetch("/api/dev-auth/login", {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: { "Content-Type": "application/json" },
|
|
154
|
+
body: JSON.stringify({ secret: devToken, ...(devUser ? { userId: devUser } : {}) }),
|
|
155
|
+
})
|
|
156
|
+
.then((r) => {
|
|
157
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
158
|
+
return r.json() as Promise<{ token: string; userId: string }>;
|
|
159
|
+
})
|
|
160
|
+
.then(async (res) => {
|
|
161
|
+
setToken(res.token);
|
|
162
|
+
dispatch({
|
|
163
|
+
type: "SET_USER",
|
|
164
|
+
user: { id: res.userId, email: devUser ? "auxtenwpc@gmail.com" : "dev@botschat.test", displayName: devUser ? "Auxten Wang" : "Dev User" },
|
|
165
|
+
});
|
|
166
|
+
const devE2e = params.get("dev_e2e");
|
|
167
|
+
if (devE2e) {
|
|
168
|
+
try {
|
|
169
|
+
await E2eService.setPassword(devE2e, res.userId, true);
|
|
170
|
+
setE2eReady(true);
|
|
171
|
+
dlog.info("Auth", "Dev E2E key derived successfully");
|
|
172
|
+
} catch (err) {
|
|
173
|
+
dlog.warn("Auth", `Dev E2E key derivation failed: ${err}`);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
E2eService.clear();
|
|
177
|
+
setE2eReady(false);
|
|
178
|
+
}
|
|
179
|
+
// Remove dev params from URL
|
|
180
|
+
params.delete("dev_token");
|
|
181
|
+
params.delete("dev_user");
|
|
182
|
+
params.delete("dev_e2e");
|
|
183
|
+
const clean = params.toString();
|
|
184
|
+
window.history.replaceState({}, "", window.location.pathname + (clean ? `?${clean}` : ""));
|
|
185
|
+
dlog.info("Auth", `Dev-token login success: ${res.userId}`);
|
|
186
|
+
})
|
|
187
|
+
.catch((err) => {
|
|
188
|
+
dlog.warn("Auth", `Dev-token login failed: ${err}`);
|
|
189
|
+
});
|
|
190
|
+
return; // skip normal auto-login
|
|
191
|
+
}
|
|
192
|
+
|
|
126
193
|
const token = getToken();
|
|
127
194
|
if (token) {
|
|
128
195
|
dlog.api("Auth", "Auto-login with stored token");
|
|
@@ -138,6 +205,35 @@ export default function App() {
|
|
|
138
205
|
setToken(null);
|
|
139
206
|
setRefreshToken(null);
|
|
140
207
|
});
|
|
208
|
+
} else if (import.meta.env.VITE_DEBUG_AUTO_LOGIN && !Capacitor.isNativePlatform()) {
|
|
209
|
+
// Debug auto-login: skip login screen in simulator/browser dev mode only.
|
|
210
|
+
// Real devices use normal Google OAuth login.
|
|
211
|
+
const debugEmail = import.meta.env.VITE_DEBUG_AUTO_LOGIN_EMAIL as string | undefined;
|
|
212
|
+
const apiBase = import.meta.env.VITE_DEBUG_API_BASE as string | undefined;
|
|
213
|
+
if (debugEmail) {
|
|
214
|
+
const url = `${apiBase || ""}/api/auth/dev-login`;
|
|
215
|
+
dlog.info("Auth", `Debug auto-login as ${debugEmail} via ${url}`);
|
|
216
|
+
fetch(url, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: { "Content-Type": "application/json" },
|
|
219
|
+
body: JSON.stringify({ email: debugEmail }),
|
|
220
|
+
})
|
|
221
|
+
.then((r) => r.json())
|
|
222
|
+
.then((res: Record<string, string>) => {
|
|
223
|
+
if (res.token) {
|
|
224
|
+
setToken(res.token);
|
|
225
|
+
if (res.refreshToken) setRefreshToken(res.refreshToken);
|
|
226
|
+
dispatch({
|
|
227
|
+
type: "SET_USER",
|
|
228
|
+
user: { id: res.id, email: res.email, displayName: res.displayName },
|
|
229
|
+
});
|
|
230
|
+
dlog.info("Auth", `Debug auto-login success: ${res.email}`);
|
|
231
|
+
} else {
|
|
232
|
+
dlog.warn("Auth", `Debug auto-login returned no token`, res);
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
.catch((err) => dlog.warn("Auth", `Debug auto-login failed: ${err}`));
|
|
236
|
+
}
|
|
141
237
|
}
|
|
142
238
|
}, []);
|
|
143
239
|
|
|
@@ -718,7 +814,14 @@ export default function App() {
|
|
|
718
814
|
client.connect();
|
|
719
815
|
wsClientRef.current = client;
|
|
720
816
|
|
|
817
|
+
// Initialize push notifications and foreground detection
|
|
818
|
+
initPushNotifications().catch((err) => {
|
|
819
|
+
dlog.warn("Push", `Push init failed: ${err}`);
|
|
820
|
+
});
|
|
821
|
+
const cleanupForeground = setupForegroundDetection(client);
|
|
822
|
+
|
|
721
823
|
return () => {
|
|
824
|
+
cleanupForeground();
|
|
722
825
|
client.disconnect();
|
|
723
826
|
wsClientRef.current = null;
|
|
724
827
|
};
|
|
@@ -774,6 +877,16 @@ export default function App() {
|
|
|
774
877
|
);
|
|
775
878
|
}
|
|
776
879
|
|
|
880
|
+
if (!dataConsentGiven) {
|
|
881
|
+
return (
|
|
882
|
+
<AppStateContext.Provider value={state}>
|
|
883
|
+
<AppDispatchContext.Provider value={dispatch}>
|
|
884
|
+
<DataConsentModal onAccept={handleDataConsent} />
|
|
885
|
+
</AppDispatchContext.Provider>
|
|
886
|
+
</AppStateContext.Provider>
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
|
|
777
890
|
// Show onboarding for new users: channels have been fetched (first API call completed)
|
|
778
891
|
// and none exist. This prevents flashing onboarding for returning users whose
|
|
779
892
|
// channel list simply hasn't loaded yet.
|
|
@@ -1021,6 +1134,9 @@ export default function App() {
|
|
|
1021
1134
|
{state.sessionModel ?? state.defaultModel ?? "Not connected"}
|
|
1022
1135
|
</span>
|
|
1023
1136
|
</div>
|
|
1137
|
+
|
|
1138
|
+
{/* Account Settings */}
|
|
1139
|
+
<AccountSettings />
|
|
1024
1140
|
</div>
|
|
1025
1141
|
)}
|
|
1026
1142
|
|
package/packages/web/src/api.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
/** Lightweight API client for the BotsChat Workers API. */
|
|
2
2
|
|
|
3
|
+
import { Capacitor } from "@capacitor/core";
|
|
3
4
|
import { dlog } from "./debug-log";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* In Capacitor (native app), the WebView loads from capacitor:// so relative
|
|
8
|
+
* URLs won't reach the API server. We use the full production URL instead.
|
|
9
|
+
* On the web, relative "/api" works because the same host serves both.
|
|
10
|
+
*/
|
|
11
|
+
const SERVER_URL = Capacitor.isNativePlatform()
|
|
12
|
+
? "https://console.botschat.app"
|
|
13
|
+
: "";
|
|
14
|
+
|
|
15
|
+
const API_BASE = `${SERVER_URL}/api`;
|
|
6
16
|
|
|
7
17
|
let _token: string | null = localStorage.getItem("botschat_token");
|
|
8
18
|
let _refreshToken: string | null = localStorage.getItem("botschat_refresh_token");
|
|
@@ -118,6 +128,7 @@ export type AuthConfig = {
|
|
|
118
128
|
emailEnabled: boolean;
|
|
119
129
|
googleEnabled: boolean;
|
|
120
130
|
githubEnabled: boolean;
|
|
131
|
+
appleEnabled: boolean;
|
|
121
132
|
};
|
|
122
133
|
|
|
123
134
|
export const authApi = {
|
|
@@ -131,6 +142,7 @@ export const authApi = {
|
|
|
131
142
|
firebase: (idToken: string) =>
|
|
132
143
|
request<AuthResponse>("POST", "/auth/firebase", { idToken }),
|
|
133
144
|
me: () => request<{ id: string; email: string; displayName: string | null; settings: UserSettings }>("GET", "/me"),
|
|
145
|
+
deleteAccount: () => request<{ ok: boolean }>("DELETE", "/auth/account"),
|
|
134
146
|
};
|
|
135
147
|
|
|
136
148
|
// ---- User settings ----
|
|
@@ -308,6 +320,14 @@ export const pairingApi = {
|
|
|
308
320
|
delete: (id: string) => request<{ ok: boolean }>("DELETE", `/pairing-tokens/${id}`),
|
|
309
321
|
};
|
|
310
322
|
|
|
323
|
+
// ---- Push Notification Tokens ----
|
|
324
|
+
export const pushApi = {
|
|
325
|
+
register: (token: string, platform: "web" | "ios" | "android") =>
|
|
326
|
+
request<{ ok: boolean; id: string }>("POST", "/push-tokens", { token, platform }),
|
|
327
|
+
unregister: (token: string) =>
|
|
328
|
+
request<{ ok: boolean }>("DELETE", "/push-tokens", { token }),
|
|
329
|
+
};
|
|
330
|
+
|
|
311
331
|
export const setupApi = {
|
|
312
332
|
/** Get the recommended cloudUrl from the backend (smart resolution). */
|
|
313
333
|
cloudUrl: () =>
|