@bodhiapp/bodhi-js 0.0.23 → 0.0.24
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/bodhi-web.cjs.js +1 -1
- package/dist/bodhi-web.esm.js +330 -236
- package/dist/direct-client.d.ts +4 -1
- package/dist/ext-client.d.ts +18 -10
- package/dist/facade-client.d.ts +3 -2
- package/package.json +4 -4
package/dist/bodhi-web.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("@bodhiapp/bodhi-js-core");class p extends s.DirectClientBase{constructor(e,t){const r=s.createStoragePrefixWithBasePath(e.basePath,s.STORAGE_PREFIXES.WEB_DIRECT),o={authClientId:e.authClientId,authServerUrl:e.authServerUrl,userScope:e.userScope,storagePrefix:r,logLevel:e.logLevel,loggerPrefix:"DirectWebClient",apiTimeoutMs:e.apiTimeoutMs};super(o,t),this.redirectUri=e.redirectUri}async login(e){const t=await this.getAuthState();if(t.status==="authenticated")return t;const r=await this.requestResourceAccess(e?.toolsetScopeIds,e?.version);if(s.isApiResultOperationError(r))throw s.createOperationError(r.error.message,r.error.type);if(s.isApiResultError(r)){const{message:S}=r.body.error;throw s.createOperationError(S,"auth_error")}if(!s.isApiResultSuccess(r))throw s.createOperationError(`Unexpected HTTP ${r.status}`,"auth_error");const o=r.body.scope;localStorage.setItem(this.storageKeys.RESOURCE_SCOPE,o);const n=r.body.toolsets||[],a=s.getMissingToolsetScopeIds(e?.toolsetScopeIds,n);if(a.length>0)throw s.createOperationError(`toolsetScopeIds not received back from request-access call: [${a.join(", ")}], check developer console on configuring the toolset scopes correctly`,"auth_error");const h=s.getRequestedToolsetScopes(e?.toolsetScopeIds,n),c=`openid profile email roles ${this.userScope} ${o} ${h}`.trim(),i=s.generateCodeVerifier(),u=await s.generateCodeChallenge(i),g=s.generateCodeVerifier();localStorage.setItem(this.storageKeys.CODE_VERIFIER,i),localStorage.setItem(this.storageKeys.STATE,g);const l=new URL(this.authEndpoints.authorize);throw l.searchParams.set("client_id",this.authClientId),l.searchParams.set("response_type","code"),l.searchParams.set("redirect_uri",this.redirectUri),l.searchParams.set("scope",c),l.searchParams.set("code_challenge",u),l.searchParams.set("code_challenge_method","S256"),l.searchParams.set("state",g),window.location.href=l.toString(),new Error("Redirect initiated")}async handleOAuthCallback(e,t){const r=localStorage.getItem(this.storageKeys.STATE);if(!r||r!==t)throw new Error("Invalid state parameter - possible CSRF attack");await this.exchangeCodeForTokens(e),localStorage.removeItem(this.storageKeys.CODE_VERIFIER),localStorage.removeItem(this.storageKeys.STATE);const o=await this.getAuthState();if(o.status!=="authenticated")throw new Error("Login failed");return this.setAuthState(o),o}async logout(){const e=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);if(e)try{const r=new URLSearchParams({token:e,client_id:this.authClientId,token_type_hint:"refresh_token"});await fetch(this.authEndpoints.revoke,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:r})}catch(r){this.logger.warn("Token revocation failed:",r)}localStorage.removeItem(this.storageKeys.ACCESS_TOKEN),localStorage.removeItem(this.storageKeys.REFRESH_TOKEN),localStorage.removeItem(this.storageKeys.EXPIRES_AT),localStorage.removeItem(this.storageKeys.RESOURCE_SCOPE);const t={status:"unauthenticated",user:null,accessToken:null,error:null};return this.setAuthState(t),t}async exchangeCodeForTokens(e){const t=localStorage.getItem(this.storageKeys.CODE_VERIFIER);if(!t)throw new Error("Code verifier not found");const r=await fetch(this.authEndpoints.token,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:this.redirectUri,client_id:this.authClientId,code_verifier:t})});if(!r.ok){const n=await r.text();throw new Error(`Token exchange failed: ${r.status} ${n}`)}const o=await r.json();if(localStorage.setItem(this.storageKeys.ACCESS_TOKEN,o.access_token),o.refresh_token&&localStorage.setItem(this.storageKeys.REFRESH_TOKEN,o.refresh_token),o.expires_in){const n=Date.now()+o.expires_in*1e3;localStorage.setItem(this.storageKeys.EXPIRES_AT,n.toString())}}async _storageGet(e){return localStorage.getItem(e)}async _storageSet(e){Object.entries(e).forEach(([t,r])=>{localStorage.setItem(t,String(r))})}async _storageRemove(e){e.forEach(t=>localStorage.removeItem(t))}_getRedirectUri(){return this.redirectUri}}const m=500,_=5e3,f=3e4;class y{constructor(e,t,r){this.state=s.EXTENSION_STATE_NOT_INITIALIZED,this.bodhiext=null,this.refreshPromise=null,this.logger=new s.Logger("WindowBodhiextClient",t.logLevel),this.authClientId=e,this.config=t,this.authEndpoints=s.createOAuthEndpoints(this.config.authServerUrl),this.onStateChange=r??s.NOOP_STATE_CALLBACK;const o=s.createStoragePrefixWithBasePath(t.basePath,s.STORAGE_PREFIXES.WEB_EXT);this.storageKeys=s.createStorageKeys(o),this.apiTimeoutMs=t.apiTimeoutMs??f}setState(e){this.state=e,this.logger.info(`{state: ${JSON.stringify(e)}} - Setting client state`),this.onStateChange({type:"client-state",state:e})}setAuthState(e){this.onStateChange({type:"auth-state",state:e})}setStateCallback(e){this.onStateChange=e}ensureBodhiext(){if(!this.bodhiext&&window.bodhiext&&(this.logger.info("Acquiring window.bodhiext reference"),this.bodhiext=window.bodhiext),!this.bodhiext)throw s.createOperationError("Client not initialized","extension_error")}async sendExtRequest(e,t){return this.ensureBodhiext(),this.bodhiext.sendExtRequest(e,t)}async sendApiRequest(e,t,r,o,n){try{this.ensureBodhiext()}catch(a){return{error:{message:a instanceof Error?a.message:String(a),type:"extension_error"}}}try{const a=new Promise((c,i)=>setTimeout(()=>i(new Error(`[bodhi-js-sdk/web] network timeout: api request not completed within configured/default timeout of ${this.apiTimeoutMs}ms`)),this.apiTimeoutMs)),h=(async()=>{let c=o||{};if(n){const i=await this._getAccessTokenRaw();if(!i)return{error:{message:"Not authenticated. Please log in first.",type:"extension_error"}};c={...c,Authorization:`Bearer ${i}`}}return this.bodhiext.sendApiRequest(e,t,r,c)})();return await Promise.race([h,a])}catch(a){const h=a?.error,c=h?.message??(a instanceof Error?a.message:String(a)),i=h?.type||"network_error";return{error:{message:c,type:i}}}}getState(){return this.state}isClientInitialized(){return this.state.extension==="ready"}isServerReady(){return this.isClientInitialized()&&this.state.server.status==="ready"}async init(e={}){if(!e.testConnection&&!e.selectedConnection)return this.logger.info("No testConnection or selectedConnection, returning not-initialized state"),s.EXTENSION_STATE_NOT_INITIALIZED;if(this.bodhiext&&!e.testConnection)return this.logger.debug("Already have bodhiext handle, skipping polling"),this.state;if(!this.bodhiext){const o=e.timeoutMs??this.config.initParams?.extension?.timeoutMs??_,n=e.intervalMs??this.config.initParams?.extension?.intervalMs??m,a=Date.now();if(!await new Promise(c=>{const i=()=>{if(window.bodhiext){this.bodhiext=window.bodhiext,c(!0);return}if(Date.now()-a>=o){c(!1);return}setTimeout(i,n)};i()}))return this.logger.warn("Extension discovery timed out"),this.setState(s.EXTENSION_STATE_NOT_FOUND),this.state}const t=await this.bodhiext.getExtensionId();this.logger.info(`Extension discovered: ${t}`);const r={type:"extension",extension:"ready",extensionId:t,server:s.PENDING_EXTENSION_READY};if(e.testConnection)try{const o=await this.getServerState();this.setState({...r,server:o}),this.logger.info(`Server connectivity tested: ${o.status}`)}catch(o){this.logger.error("Failed to get server state:",o),this.setState({...r,server:s.BACKEND_SERVER_NOT_REACHABLE})}else this.setState(r);return this.state}async requestResourceAccess(e,t){this.ensureBodhiext();const r={app_client_id:this.authClientId,...e&&{toolset_scope_ids:e},...t&&{version:t}};return this.bodhiext.sendApiRequest("POST","/bodhi/v1/apps/request-access",r)}async login(e){const t=await this.getAuthState();if(t.status==="authenticated")return t;this.ensureBodhiext();const r=await this.requestResourceAccess(e?.toolsetScopeIds,e?.version);if(s.isApiResultOperationError(r))throw s.createOperationError(r.error.message,r.error.type);if(s.isApiResultError(r)){const{message:E}=r.body.error;throw s.createOperationError(E,"auth_error")}if(!s.isApiResultSuccess(r))throw s.createOperationError(`Unexpected HTTP ${r.status}`,"auth_error");const o=r.body.scope;localStorage.setItem(this.storageKeys.RESOURCE_SCOPE,o);const n=r.body.toolsets||[],a=s.getMissingToolsetScopeIds(e?.toolsetScopeIds,n);if(a.length>0)throw s.createOperationError(`toolsetScopeIds not received back from request-access call: [${a.join(", ")}], check developer console on configuring the toolset scopes correctly`,"auth_error");const h=s.getRequestedToolsetScopes(e?.toolsetScopeIds,n),c=s.generateCodeVerifier(),i=await s.generateCodeChallenge(c),u=s.generateCodeVerifier();localStorage.setItem(this.storageKeys.CODE_VERIFIER,c),localStorage.setItem(this.storageKeys.STATE,u);const g=["openid","profile","email","roles",this.config.userScope,o,...h?h.split(" "):[]],l=new URLSearchParams({response_type:"code",client_id:this.authClientId,redirect_uri:this.config.redirectUri,scope:g.join(" "),state:u,code_challenge:i,code_challenge_method:"S256"}),S=`${this.authEndpoints.authorize}?${l}`;return window.location.href=S,new Promise(()=>{})}async handleOAuthCallback(e,t){const r=localStorage.getItem(this.storageKeys.STATE);if(!r||r!==t)throw new Error("Invalid state parameter - possible CSRF attack");await this.exchangeCodeForTokens(e),localStorage.removeItem(this.storageKeys.CODE_VERIFIER),localStorage.removeItem(this.storageKeys.STATE);const o=await this.getAuthState();if(o.status!=="authenticated")throw new Error("Login failed");return this.setAuthState(o),o}async exchangeCodeForTokens(e){const t=localStorage.getItem(this.storageKeys.CODE_VERIFIER);if(!t)throw new Error("Code verifier not found");const r=new URLSearchParams({grant_type:"authorization_code",client_id:this.authClientId,code:e,redirect_uri:this.config.redirectUri,code_verifier:t}),o=await fetch(this.authEndpoints.token,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:r});if(!o.ok){const a=await o.text();throw new Error(`Token exchange failed: ${o.status} ${a}`)}const n=await o.json();if(!n.access_token)throw new Error("No access token received");if(localStorage.setItem(this.storageKeys.ACCESS_TOKEN,n.access_token),n.refresh_token&&localStorage.setItem(this.storageKeys.REFRESH_TOKEN,n.refresh_token),n.expires_in){const a=Date.now()+n.expires_in*1e3;localStorage.setItem(this.storageKeys.EXPIRES_AT,a.toString())}}async logout(){const e=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);if(e)try{const r=new URLSearchParams({token:e,client_id:this.authClientId,token_type_hint:"refresh_token"});await fetch(this.authEndpoints.revoke,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:r})}catch(r){this.logger.warn("Token revocation failed:",r)}localStorage.removeItem(this.storageKeys.ACCESS_TOKEN),localStorage.removeItem(this.storageKeys.REFRESH_TOKEN),localStorage.removeItem(this.storageKeys.EXPIRES_AT),localStorage.removeItem(this.storageKeys.CODE_VERIFIER),localStorage.removeItem(this.storageKeys.STATE),localStorage.removeItem(this.storageKeys.RESOURCE_SCOPE);const t={status:"unauthenticated",user:null,accessToken:null,error:null};return this.setAuthState(t),t}async getAuthState(){const e=await this._getAccessTokenRaw();if(!e)return{status:"unauthenticated",user:null,accessToken:null,error:null};try{return{status:"authenticated",user:s.extractUserInfo(e),accessToken:e,error:null}}catch(t){return this.logger.error("Failed to parse token:",t),{status:"unauthenticated",user:null,accessToken:null,error:null}}}async _getAccessTokenRaw(){const e=localStorage.getItem(this.storageKeys.ACCESS_TOKEN),t=localStorage.getItem(this.storageKeys.EXPIRES_AT);if(!e)return null;if(t){const r=parseInt(t,10);if(Date.now()>=r-5*1e3){const o=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);return o?this._tryRefreshToken(o):null}}return e}async _tryRefreshToken(e){if(this.refreshPromise)return this.logger.debug("Refresh already in progress, returning existing promise"),this.refreshPromise;this.refreshPromise=this._doRefreshToken(e);try{return await this.refreshPromise}finally{this.refreshPromise=null}}async _doRefreshToken(e){this.logger.debug("Refreshing access token");try{const t=await s.refreshAccessToken(this.authEndpoints.token,e,this.authClientId);if(t.success){this._storeRefreshedTokens(t.tokens);const r=s.extractUserInfo(t.tokens.access_token);return this.setAuthState({status:"authenticated",user:r,accessToken:t.tokens.access_token,error:null}),this.logger.info("Token refreshed successfully"),t.tokens.access_token}if(t.error==="invalid_grant")return this.logger.warn("Refresh token expired or revoked, clearing tokens and logging out"),this.clearAuthStorage(),this.setAuthState({status:"unauthenticated",user:null,accessToken:null,error:null}),null}catch(t){this.logger.warn("Token refresh failed:",t)}throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"),s.createOperationError("Access token expired and unable to refresh. Try logging out and logging in again.","token_refresh_failed")}clearAuthStorage(){localStorage.removeItem(this.storageKeys.ACCESS_TOKEN),localStorage.removeItem(this.storageKeys.REFRESH_TOKEN),localStorage.removeItem(this.storageKeys.EXPIRES_AT),localStorage.removeItem(this.storageKeys.RESOURCE_SCOPE)}_storeRefreshedTokens(e){const t=Date.now()+e.expires_in*1e3;localStorage.setItem(this.storageKeys.ACCESS_TOKEN,e.access_token),localStorage.setItem(this.storageKeys.EXPIRES_AT,String(t)),e.refresh_token&&localStorage.setItem(this.storageKeys.REFRESH_TOKEN,e.refresh_token)}async pingApi(){return this.sendApiRequest("GET","/ping")}async getServerState(){const e=await this.sendApiRequest("GET","/bodhi/v1/info");if(s.isApiResultOperationError(e)||!s.isApiResultSuccess(e))return s.BACKEND_SERVER_NOT_REACHABLE;const t=e.body;switch(t.status){case"ready":return{status:"ready",version:t.version||"unknown",error:null};case"setup":return s.backendServerNotReady("setup",t.version||"unknown");case"resource-admin":return s.backendServerNotReady("resource-admin",t.version||"unknown");case"error":return s.backendServerNotReady("error",t.version||"unknown",t.error?{message:t.error.message,type:t.error.type}:s.SERVER_ERROR_CODES.SERVER_NOT_READY);default:return s.BACKEND_SERVER_NOT_REACHABLE}}async*stream(e,t,r,o,n=!0){this.ensureBodhiext();let a=o||{};if(n){const i=await this._getAccessTokenRaw();if(!i)throw s.createOperationError("Not authenticated. Please log in first.","auth_error");a={...a,Authorization:`Bearer ${i}`}}const c=this.bodhiext.sendStreamRequest(e,t,r,a).getReader();try{for(;;){const{value:i,done:u}=await c.read();if(u||i?.done)break;yield i.body}}catch(i){if(i instanceof Error){if("response"in i){const u=i;throw s.createApiError(i.message,u.response.status,u.response.body)}throw"error"in i,s.createOperationError(i.message,"extension_error")}throw i}finally{c.releaseLock()}}get chat(){return this._chat??=new s.Chat(this)}get models(){return this._models??=new s.Models(this)}get embeddings(){return this._embeddings??=new s.Embeddings(this)}serialize(){return{extensionId:this.state.type==="extension"&&this.state.extension==="ready"?this.state.extensionId:void 0}}async debug(){return{type:"WindowBodhiextClient",state:this.state,authState:await this.getAuthState(),bodhiextAvailable:this.bodhiext!==null,authClientId:this.authClientId,authServerUrl:this.config.authServerUrl,redirectUri:this.config.redirectUri,userScope:this.config.userScope}}}function T(d){if(typeof window>"u")throw new Error("redirectUri required in non-browser environment");const e=d==="/"?"":d.replace(/\/$/,"");return`${window.location.origin}${e}/callback`}class w extends s.BaseFacadeClient{constructor(e,t,r){const o=t||{},n={basePath:o.basePath||"/",redirectUri:o.redirectUri||T(o.basePath||"/"),authServerUrl:o.authServerUrl||"https://id.getbodhi.app/realms/bodhi",userScope:o.userScope||"scope_user_user",logLevel:o.logLevel||"warn",apiTimeoutMs:o.apiTimeoutMs,initParams:o.initParams};super(e,n,r)}createLogger(e){return new s.Logger("WebUIClient",e.logLevel)}createStoragePrefix(e){return s.createStoragePrefixWithBasePath(e.basePath,s.STORAGE_PREFIXES.WEB)}createExtClient(e,t){return new y(this.authClientId,{authServerUrl:e.authServerUrl,redirectUri:e.redirectUri,userScope:e.userScope,basePath:e.basePath,logLevel:e.logLevel,apiTimeoutMs:e.apiTimeoutMs,initParams:e.initParams},t)}createDirectClient(e,t,r){return new p({authClientId:e,authServerUrl:t.authServerUrl,redirectUri:t.redirectUri,userScope:t.userScope,logLevel:t.logLevel,basePath:t.basePath,apiTimeoutMs:t.apiTimeoutMs},r)}async handleOAuthCallback(e,t){return this.connectionMode==="direct"?this.directClient.handleOAuthCallback(e,t):this.extClient.handleOAuthCallback(e,t)}}const I="production";exports.WEB_BUILD_MODE=I;exports.WebUIClient=w;
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("@bodhiapp/bodhi-js-core");class _ extends r.DirectClientBase{constructor(e,t){const s=r.createStoragePrefixWithBasePath(e.basePath,r.STORAGE_PREFIXES.WEB_DIRECT),o={authClientId:e.authClientId,authServerUrl:e.authServerUrl,userRole:e.userRole,storagePrefix:s,logLevel:e.logLevel,loggerPrefix:"DirectWebClient",apiTimeoutMs:e.apiTimeoutMs};super(o,t),this.redirectUri=e.redirectUri}async login(e){const t=await this.getAuthState();if(t.status==="authenticated")return t;const s=e?.userRole??this.userRole,o=e?.flowType??"popup";e?.onProgress?.("requesting");const i=new r.AccessRequestBuilder(this.authClientId).requestedRole(s).flowType(o);if(e?.requested&&i.requested(e.requested),o==="redirect"){const u=e?.redirectUrl??this.redirectUri;i.redirectUrl(u)}const n=i.build(),c=await this.requestAccess(n);if(r.isApiResultOperationError(c))throw r.createOperationError(c.error.message,c.error.type);if(!r.isApiResultSuccess(c))throw r.createOperationError(`Access request failed: HTTP ${c.status}`,"auth_error");const{id:l,review_url:a}=c.body;e?.onProgress?.("reviewing");let h;if(o==="popup"){const u=async()=>{const p=await this.getAccessRequestStatus(l);if(!r.isApiResultSuccess(p))return null;const{status:E,access_request_scope:S}=p.body;return E==="approved"?{approved:!0,accessRequestScope:S??void 0}:["denied","failed","expired"].includes(E)?{approved:!1}:null},g=await r.openPopupReview(a,u,{intervalMs:e?.pollIntervalMs??r.DEFAULT_POLL_INTERVAL_MS,timeoutMs:e?.pollTimeoutMs??r.DEFAULT_POLL_TIMEOUT_MS});if(!g.approved)throw r.createOperationError("Access request was denied or expired","auth_error");h=g.accessRequestScope}else return localStorage.setItem(this.storageKeys.ACCESS_REQUEST_ID,l),window.location.href=a,new Promise(()=>{});return e?.onProgress?.("authenticating"),this.performOAuthPkce(`openid profile email roles ${h??""}`.trim())}async performOAuthPkce(e){const t=r.generateCodeVerifier(),s=await r.generateCodeChallenge(t),o=r.generateCodeVerifier();localStorage.setItem(this.storageKeys.CODE_VERIFIER,t),localStorage.setItem(this.storageKeys.STATE,o);const i=new URL(this.authEndpoints.authorize);throw i.searchParams.set("client_id",this.authClientId),i.searchParams.set("response_type","code"),i.searchParams.set("redirect_uri",this.redirectUri),i.searchParams.set("scope",e),i.searchParams.set("code_challenge",s),i.searchParams.set("code_challenge_method","S256"),i.searchParams.set("state",o),window.location.href=i.toString(),new Error("Redirect initiated")}async handleOAuthCallback(e,t){const s=localStorage.getItem(this.storageKeys.STATE);if(!s||s!==t)throw new Error("Invalid state parameter - possible CSRF attack");await this.exchangeCodeForTokens(e),localStorage.removeItem(this.storageKeys.CODE_VERIFIER),localStorage.removeItem(this.storageKeys.STATE);const o=await this.getAuthState();if(o.status!=="authenticated")throw new Error("Login failed");return this.setAuthState(o),o}async handleAccessRequestCallback(e){const t=await this.getAccessRequestStatus(e);if(!r.isApiResultSuccess(t))throw r.createOperationError("Failed to get access request status","auth_error");const{status:s,access_request_scope:o}=t.body;if(s!=="approved")throw r.createOperationError(`Access request is not approved: ${s}`,"auth_error");const i=`openid profile email roles ${o??""}`.trim();return localStorage.removeItem(this.storageKeys.ACCESS_REQUEST_ID),this.performOAuthPkce(i)}async logout(){const e=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);if(e)try{const s=new URLSearchParams({token:e,client_id:this.authClientId,token_type_hint:"refresh_token"});await fetch(this.authEndpoints.revoke,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:s})}catch(s){this.logger.warn("Token revocation failed:",s)}localStorage.removeItem(this.storageKeys.ACCESS_TOKEN),localStorage.removeItem(this.storageKeys.REFRESH_TOKEN),localStorage.removeItem(this.storageKeys.EXPIRES_AT);const t={status:"unauthenticated",user:null,accessToken:null,error:null};return this.setAuthState(t),t}async exchangeCodeForTokens(e){const t=localStorage.getItem(this.storageKeys.CODE_VERIFIER);if(!t)throw new Error("Code verifier not found");const s=await fetch(this.authEndpoints.token,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:this.redirectUri,client_id:this.authClientId,code_verifier:t})});if(!s.ok){const i=await s.text();throw new Error(`Token exchange failed: ${s.status} ${i}`)}const o=await s.json();if(localStorage.setItem(this.storageKeys.ACCESS_TOKEN,o.access_token),o.refresh_token&&localStorage.setItem(this.storageKeys.REFRESH_TOKEN,o.refresh_token),o.expires_in){const i=Date.now()+o.expires_in*1e3;localStorage.setItem(this.storageKeys.EXPIRES_AT,i.toString())}}async _storageGet(e){return localStorage.getItem(e)}async _storageSet(e){Object.entries(e).forEach(([t,s])=>{localStorage.setItem(t,String(s))})}async _storageRemove(e){e.forEach(t=>localStorage.removeItem(t))}_getRedirectUri(){return this.redirectUri}}const f=500,m=5e3,R=3e4;class w{constructor(e,t,s){this.state=r.EXTENSION_STATE_NOT_INITIALIZED,this.bodhiext=null,this.refreshPromise=null,this.logger=new r.Logger("WindowBodhiextClient",t.logLevel),this.authClientId=e,this.config=t,this.authEndpoints=r.createOAuthEndpoints(this.config.authServerUrl),this.onStateChange=s??r.NOOP_STATE_CALLBACK;const o=r.createStoragePrefixWithBasePath(t.basePath,r.STORAGE_PREFIXES.WEB_EXT);this.storageKeys=r.createStorageKeys(o),this.apiTimeoutMs=t.apiTimeoutMs??R}setState(e){this.state=e,this.logger.info(`{state: ${JSON.stringify(e)}} - Setting client state`),this.onStateChange({type:"client-state",state:e})}setAuthState(e){this.onStateChange({type:"auth-state",state:e})}setStateCallback(e){this.onStateChange=e}ensureBodhiext(){if(!this.bodhiext&&window.bodhiext&&(this.logger.info("Acquiring window.bodhiext reference"),this.bodhiext=window.bodhiext),!this.bodhiext)throw r.createOperationError("Client not initialized","extension_error")}async sendExtRequest(e,t){return this.ensureBodhiext(),this.bodhiext.sendExtRequest(e,t)}async sendApiRequest(e,t,s,o,i){try{this.ensureBodhiext()}catch(n){return{error:{message:n instanceof Error?n.message:String(n),type:"extension_error"}}}try{const n=new Promise((l,a)=>setTimeout(()=>a(new Error(`[bodhi-js-sdk/web] network timeout: api request not completed within configured/default timeout of ${this.apiTimeoutMs}ms`)),this.apiTimeoutMs)),c=(async()=>{let l=o||{};if(i){const a=await this._getAccessTokenRaw();if(!a)return{error:{message:"Not authenticated. Please log in first.",type:"extension_error"}};l={...l,Authorization:`Bearer ${a}`}}return this.bodhiext.sendApiRequest(e,t,s,l)})();return await Promise.race([c,n])}catch(n){const c=n?.error,l=c?.message??(n instanceof Error?n.message:String(n)),a=c?.type||"network_error";return{error:{message:l,type:a}}}}getState(){return this.state}isClientInitialized(){return this.state.extension==="ready"}isServerReady(){return this.isClientInitialized()&&this.state.server.status==="ready"}async init(e={}){if(!e.testConnection&&!e.selectedConnection)return this.logger.info("No testConnection or selectedConnection, returning not-initialized state"),r.EXTENSION_STATE_NOT_INITIALIZED;if(this.bodhiext&&!e.testConnection)return this.logger.debug("Already have bodhiext handle, skipping polling"),this.state;if(!this.bodhiext){const o=e.timeoutMs??this.config.initParams?.extension?.timeoutMs??m,i=e.intervalMs??this.config.initParams?.extension?.intervalMs??f,n=Date.now();if(!await new Promise(l=>{const a=()=>{if(window.bodhiext){this.bodhiext=window.bodhiext,l(!0);return}if(Date.now()-n>=o){l(!1);return}setTimeout(a,i)};a()}))return this.logger.warn("Extension discovery timed out"),this.setState(r.EXTENSION_STATE_NOT_FOUND),this.state}const t=await this.bodhiext.getExtensionId();this.logger.info(`Extension discovered: ${t}`);const s={type:"extension",extension:"ready",extensionId:t,server:r.PENDING_EXTENSION_READY};if(e.testConnection)try{const o=await this.getServerState();this.setState({...s,server:o}),this.logger.info(`Server connectivity tested: ${o.status}`)}catch(o){this.logger.error("Failed to get server state:",o),this.setState({...s,server:r.BACKEND_SERVER_NOT_REACHABLE})}else this.setState(s);return this.state}async login(e){const t=await this.getAuthState();if(t.status==="authenticated")return t;this.ensureBodhiext();const s=e?.userRole??this.config.userRole,o=e?.flowType??"popup";e?.onProgress?.("requesting");const i=new r.AccessRequestBuilder(this.authClientId).requestedRole(s).flowType(o);if(e?.requested&&i.requested(e.requested),o==="redirect"){const u=e?.redirectUrl??this.config.redirectUri;i.redirectUrl(u)}const n=i.build(),c=await this.requestAccess(n);if(r.isApiResultOperationError(c))throw r.createOperationError(c.error.message,c.error.type);if(!r.isApiResultSuccess(c))throw r.createOperationError(`Access request failed: HTTP ${c.status}`,"auth_error");const{id:l,review_url:a}=c.body;e?.onProgress?.("reviewing");let h;if(o==="popup"){const u=async()=>{const p=await this.getAccessRequestStatus(l);if(!r.isApiResultSuccess(p))return null;const{status:E,access_request_scope:S}=p.body;return E==="approved"?{approved:!0,accessRequestScope:S??void 0}:["denied","failed","expired"].includes(E)?{approved:!1}:null},g=await r.openPopupReview(a,u,{intervalMs:e?.pollIntervalMs??r.DEFAULT_POLL_INTERVAL_MS,timeoutMs:e?.pollTimeoutMs??r.DEFAULT_POLL_TIMEOUT_MS});if(!g.approved)throw r.createOperationError("Access request was denied or expired","auth_error");h=g.accessRequestScope}else return localStorage.setItem(this.storageKeys.ACCESS_REQUEST_ID,l),window.location.href=a,new Promise(()=>{});return e?.onProgress?.("authenticating"),this.performOAuthPkce(`openid profile email roles ${h??""}`.trim())}async handleOAuthCallback(e,t){const s=localStorage.getItem(this.storageKeys.STATE);if(!s||s!==t)throw new Error("Invalid state parameter - possible CSRF attack");await this.exchangeCodeForTokens(e),localStorage.removeItem(this.storageKeys.CODE_VERIFIER),localStorage.removeItem(this.storageKeys.STATE);const o=await this.getAuthState();if(o.status!=="authenticated")throw new Error("Login failed");return this.setAuthState(o),o}async exchangeCodeForTokens(e){const t=localStorage.getItem(this.storageKeys.CODE_VERIFIER);if(!t)throw new Error("Code verifier not found");const s=new URLSearchParams({grant_type:"authorization_code",client_id:this.authClientId,code:e,redirect_uri:this.config.redirectUri,code_verifier:t}),o=await fetch(this.authEndpoints.token,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:s});if(!o.ok){const n=await o.text();throw new Error(`Token exchange failed: ${o.status} ${n}`)}const i=await o.json();if(!i.access_token)throw new Error("No access token received");if(localStorage.setItem(this.storageKeys.ACCESS_TOKEN,i.access_token),i.refresh_token&&localStorage.setItem(this.storageKeys.REFRESH_TOKEN,i.refresh_token),i.expires_in){const n=Date.now()+i.expires_in*1e3;localStorage.setItem(this.storageKeys.EXPIRES_AT,n.toString())}}async logout(){const e=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);if(e)try{const s=new URLSearchParams({token:e,client_id:this.authClientId,token_type_hint:"refresh_token"});await fetch(this.authEndpoints.revoke,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:s})}catch(s){this.logger.warn("Token revocation failed:",s)}localStorage.removeItem(this.storageKeys.ACCESS_TOKEN),localStorage.removeItem(this.storageKeys.REFRESH_TOKEN),localStorage.removeItem(this.storageKeys.EXPIRES_AT),localStorage.removeItem(this.storageKeys.CODE_VERIFIER),localStorage.removeItem(this.storageKeys.STATE);const t={status:"unauthenticated",user:null,accessToken:null,error:null};return this.setAuthState(t),t}async getAuthState(){const e=await this._getAccessTokenRaw();if(!e)return{status:"unauthenticated",user:null,accessToken:null,error:null};try{return{status:"authenticated",user:r.extractUserInfo(e),accessToken:e,error:null}}catch(t){return this.logger.error("Failed to parse token:",t),{status:"unauthenticated",user:null,accessToken:null,error:null}}}async _getAccessTokenRaw(){const e=localStorage.getItem(this.storageKeys.ACCESS_TOKEN),t=localStorage.getItem(this.storageKeys.EXPIRES_AT);if(!e)return null;if(t){const s=parseInt(t,10);if(Date.now()>=s-5*1e3){const o=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);return o?this._tryRefreshToken(o):null}}return e}async _tryRefreshToken(e){if(this.refreshPromise)return this.logger.debug("Refresh already in progress, returning existing promise"),this.refreshPromise;this.refreshPromise=this._doRefreshToken(e);try{return await this.refreshPromise}finally{this.refreshPromise=null}}async _doRefreshToken(e){this.logger.debug("Refreshing access token");try{const t=await r.refreshAccessToken(this.authEndpoints.token,e,this.authClientId);if(t.success){this._storeRefreshedTokens(t.tokens);const s=r.extractUserInfo(t.tokens.access_token);return this.setAuthState({status:"authenticated",user:s,accessToken:t.tokens.access_token,error:null}),this.logger.info("Token refreshed successfully"),t.tokens.access_token}if(t.error==="invalid_grant")return this.logger.warn("Refresh token expired or revoked, clearing tokens and logging out"),this.clearAuthStorage(),this.setAuthState({status:"unauthenticated",user:null,accessToken:null,error:null}),null}catch(t){this.logger.warn("Token refresh failed:",t)}throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"),r.createOperationError("Access token expired and unable to refresh. Try logging out and logging in again.","token_refresh_failed")}clearAuthStorage(){localStorage.removeItem(this.storageKeys.ACCESS_TOKEN),localStorage.removeItem(this.storageKeys.REFRESH_TOKEN),localStorage.removeItem(this.storageKeys.EXPIRES_AT)}_storeRefreshedTokens(e){const t=Date.now()+e.expires_in*1e3;localStorage.setItem(this.storageKeys.ACCESS_TOKEN,e.access_token),localStorage.setItem(this.storageKeys.EXPIRES_AT,String(t)),e.refresh_token&&localStorage.setItem(this.storageKeys.REFRESH_TOKEN,e.refresh_token)}async pingApi(){return this.sendApiRequest("GET","/ping")}async getServerState(){const e=await this.sendApiRequest("GET","/bodhi/v1/info");if(r.isApiResultOperationError(e)||!r.isApiResultSuccess(e))return r.BACKEND_SERVER_NOT_REACHABLE;const t=e.body,s=t.version||"unknown";switch(t.status){case"ready":return{status:"ready",version:s,error:null,deployment:t.deployment??null,client_id:t.client_id??null};case"setup":return r.backendServerNotReady("setup",s,void 0,t.deployment,t.client_id);case"resource_admin":return r.backendServerNotReady("resource_admin",s,void 0,t.deployment,t.client_id);case"tenant_selection":return{status:"tenant_selection",version:s,error:null,deployment:t.deployment??null,client_id:t.client_id??null};case"error":return r.backendServerNotReady("error",s,t.error?{message:t.error.message,type:t.error.type}:r.SERVER_ERROR_CODES.SERVER_NOT_READY,t.deployment,t.client_id);default:return r.BACKEND_SERVER_NOT_REACHABLE}}async*stream(e,t,s,o,i=!0){this.ensureBodhiext();let n=o||{};if(i){const a=await this._getAccessTokenRaw();if(!a)throw r.createOperationError("Not authenticated. Please log in first.","auth_error");n={...n,Authorization:`Bearer ${a}`}}const l=this.bodhiext.sendStreamRequest(e,t,s,n).getReader();try{for(;;){const{value:a,done:h}=await l.read();if(h||a?.done)break;yield a.body}}catch(a){if(a instanceof Error){if("response"in a){const h=a;throw r.createApiError(a.message,h.response.status,h.response.body)}throw"error"in a,r.createOperationError(a.message,"extension_error")}throw a}finally{l.releaseLock()}}get chat(){return this._chat??=new r.Chat(this)}get models(){return this._models??=new r.Models(this)}get embeddings(){return this._embeddings??=new r.Embeddings(this)}get toolsets(){return this._toolsets??=new r.Toolsets(this)}get mcps(){return this._mcps??=new r.Mcps(this)}async requestAccess(e){return this.sendApiRequest("POST","/bodhi/v1/apps/request-access",e,{},!1)}async getAccessRequestStatus(e){return this.sendApiRequest("GET",`/bodhi/v1/apps/access-requests/${e}?app_client_id=${encodeURIComponent(this.authClientId)}`,void 0,{},!1)}async pollAccessRequestStatus(e,t){return r.pollAccessRequestUntilResolved(s=>this.getAccessRequestStatus(s),e,t)}async performOAuthPkce(e){const t=r.generateCodeVerifier(),s=await r.generateCodeChallenge(t),o=r.generateCodeVerifier();localStorage.setItem(this.storageKeys.CODE_VERIFIER,t),localStorage.setItem(this.storageKeys.STATE,o);const i=e.split(" ").filter(Boolean),n=new URLSearchParams({response_type:"code",client_id:this.authClientId,redirect_uri:this.config.redirectUri,scope:i.join(" "),state:o,code_challenge:s,code_challenge_method:"S256"});return window.location.href=`${this.authEndpoints.authorize}?${n}`,new Promise(()=>{})}async handleAccessRequestCallback(e){const t=await this.getAccessRequestStatus(e);if(!r.isApiResultSuccess(t))throw r.createOperationError("Failed to get access request status","auth_error");const{status:s,access_request_scope:o}=t.body;if(s!=="approved")throw r.createOperationError(`Access request is not approved: ${s}`,"auth_error");const i=`openid profile email roles ${o??""}`.trim();return localStorage.removeItem(this.storageKeys.ACCESS_REQUEST_ID),this.performOAuthPkce(i)}serialize(){return{extensionId:this.state.type==="extension"&&this.state.extension==="ready"?this.state.extensionId:void 0}}async debug(){return{type:"WindowBodhiextClient",state:this.state,authState:await this.getAuthState(),bodhiextAvailable:this.bodhiext!==null,authClientId:this.authClientId,authServerUrl:this.config.authServerUrl,redirectUri:this.config.redirectUri,userRole:this.config.userRole}}}function y(d){if(typeof window>"u")throw new Error("redirectUri required in non-browser environment");const e=d==="/"?"":d.replace(/\/$/,"");return`${window.location.origin}${e}/callback`}class T extends r.BaseFacadeClient{constructor(e,t,s){const o=t||{},i={basePath:o.basePath||"/",redirectUri:o.redirectUri||y(o.basePath||"/"),authServerUrl:o.authServerUrl||"https://id.getbodhi.app/realms/bodhi",userRole:o.userRole||"scope_user_user",logLevel:o.logLevel||"warn",apiTimeoutMs:o.apiTimeoutMs,initParams:o.initParams};super(e,i,s)}createLogger(e){return new r.Logger("WebUIClient",e.logLevel)}createStoragePrefix(e){return r.createStoragePrefixWithBasePath(e.basePath,r.STORAGE_PREFIXES.WEB)}createExtClient(e,t){return new w(this.authClientId,{authServerUrl:e.authServerUrl,redirectUri:e.redirectUri,userRole:e.userRole,basePath:e.basePath,logLevel:e.logLevel,apiTimeoutMs:e.apiTimeoutMs,initParams:e.initParams},t)}createDirectClient(e,t,s){return new _({authClientId:e,authServerUrl:t.authServerUrl,redirectUri:t.redirectUri,userRole:t.userRole,logLevel:t.logLevel,basePath:t.basePath,apiTimeoutMs:t.apiTimeoutMs},s)}async handleOAuthCallback(e,t){return this.connectionMode==="direct"?this.directClient.handleOAuthCallback(e,t):this.extClient.handleOAuthCallback(e,t)}async handleAccessRequestCallback(e){return this.connectionMode==="direct"?this.directClient.handleAccessRequestCallback(e):this.extClient.handleAccessRequestCallback(e)}}const A="production";exports.WEB_BUILD_MODE=A;exports.WebUIClient=T;
|
package/dist/bodhi-web.esm.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { DirectClientBase as
|
|
2
|
-
class
|
|
1
|
+
import { DirectClientBase as O, createStoragePrefixWithBasePath as R, STORAGE_PREFIXES as T, AccessRequestBuilder as C, isApiResultOperationError as y, createOperationError as l, isApiResultSuccess as u, openPopupReview as v, DEFAULT_POLL_TIMEOUT_MS as k, DEFAULT_POLL_INTERVAL_MS as x, generateCodeVerifier as _, generateCodeChallenge as b, EXTENSION_STATE_NOT_INITIALIZED as I, Logger as P, createOAuthEndpoints as q, NOOP_STATE_CALLBACK as U, createStorageKeys as K, EXTENSION_STATE_NOT_FOUND as L, PENDING_EXTENSION_READY as N, BACKEND_SERVER_NOT_REACHABLE as m, extractUserInfo as A, refreshAccessToken as D, backendServerNotReady as w, SERVER_ERROR_CODES as M, createApiError as F, Chat as B, Models as $, Embeddings as V, Toolsets as H, Mcps as z, pollAccessRequestUntilResolved as W, BaseFacadeClient as X } from "@bodhiapp/bodhi-js-core";
|
|
2
|
+
class j extends O {
|
|
3
3
|
constructor(e, t) {
|
|
4
|
-
const
|
|
4
|
+
const s = R(
|
|
5
5
|
e.basePath,
|
|
6
|
-
|
|
7
|
-
),
|
|
6
|
+
T.WEB_DIRECT
|
|
7
|
+
), r = {
|
|
8
8
|
authClientId: e.authClientId,
|
|
9
9
|
authServerUrl: e.authServerUrl,
|
|
10
|
-
|
|
11
|
-
storagePrefix:
|
|
10
|
+
userRole: e.userRole,
|
|
11
|
+
storagePrefix: s,
|
|
12
12
|
logLevel: e.logLevel,
|
|
13
13
|
loggerPrefix: "DirectWebClient",
|
|
14
14
|
apiTimeoutMs: e.apiTimeoutMs
|
|
15
15
|
};
|
|
16
|
-
super(
|
|
16
|
+
super(r, t), this.redirectUri = e.redirectUri;
|
|
17
17
|
}
|
|
18
18
|
// ============================================================================
|
|
19
19
|
// Authentication (Browser Redirect OAuth)
|
|
@@ -22,43 +22,73 @@ class V extends b {
|
|
|
22
22
|
const t = await this.getAuthState();
|
|
23
23
|
if (t.status === "authenticated")
|
|
24
24
|
return t;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
const
|
|
30
|
-
|
|
25
|
+
const s = e?.userRole ?? this.userRole, r = e?.flowType ?? "popup";
|
|
26
|
+
e?.onProgress?.("requesting");
|
|
27
|
+
const o = new C(this.authClientId).requestedRole(s).flowType(r);
|
|
28
|
+
if (e?.requested && o.requested(e.requested), r === "redirect") {
|
|
29
|
+
const g = e?.redirectUrl ?? this.redirectUri;
|
|
30
|
+
o.redirectUrl(g);
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
throw c(
|
|
39
|
-
`toolsetScopeIds not received back from request-access call: [${i.join(", ")}], check developer console on configuring the toolset scopes correctly`,
|
|
32
|
+
const a = o.build(), n = await this.requestAccess(a);
|
|
33
|
+
if (y(n))
|
|
34
|
+
throw l(n.error.message, n.error.type);
|
|
35
|
+
if (!u(n))
|
|
36
|
+
throw l(
|
|
37
|
+
`Access request failed: HTTP ${n.status}`,
|
|
40
38
|
"auth_error"
|
|
41
39
|
);
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
const { id: c, review_url: i } = n.body;
|
|
41
|
+
e?.onProgress?.("reviewing");
|
|
42
|
+
let h;
|
|
43
|
+
if (r === "popup") {
|
|
44
|
+
const p = await v(i, async () => {
|
|
45
|
+
const S = await this.getAccessRequestStatus(c);
|
|
46
|
+
if (!u(S)) return null;
|
|
47
|
+
const { status: f, access_request_scope: E } = S.body;
|
|
48
|
+
return f === "approved" ? { approved: !0, accessRequestScope: E ?? void 0 } : ["denied", "failed", "expired"].includes(f) ? { approved: !1 } : null;
|
|
49
|
+
}, {
|
|
50
|
+
intervalMs: e?.pollIntervalMs ?? x,
|
|
51
|
+
timeoutMs: e?.pollTimeoutMs ?? k
|
|
52
|
+
});
|
|
53
|
+
if (!p.approved)
|
|
54
|
+
throw l("Access request was denied or expired", "auth_error");
|
|
55
|
+
h = p.accessRequestScope;
|
|
56
|
+
} else
|
|
57
|
+
return localStorage.setItem(this.storageKeys.ACCESS_REQUEST_ID, c), window.location.href = i, new Promise(() => {
|
|
58
|
+
});
|
|
59
|
+
return e?.onProgress?.("authenticating"), this.performOAuthPkce(`openid profile email roles ${h ?? ""}`.trim());
|
|
60
|
+
}
|
|
61
|
+
async performOAuthPkce(e) {
|
|
62
|
+
const t = _(), s = await b(t), r = _();
|
|
63
|
+
localStorage.setItem(this.storageKeys.CODE_VERIFIER, t), localStorage.setItem(this.storageKeys.STATE, r);
|
|
64
|
+
const o = new URL(this.authEndpoints.authorize);
|
|
65
|
+
throw o.searchParams.set("client_id", this.authClientId), o.searchParams.set("response_type", "code"), o.searchParams.set("redirect_uri", this.redirectUri), o.searchParams.set("scope", e), o.searchParams.set("code_challenge", s), o.searchParams.set("code_challenge_method", "S256"), o.searchParams.set("state", r), window.location.href = o.toString(), new Error("Redirect initiated");
|
|
46
66
|
}
|
|
47
67
|
async handleOAuthCallback(e, t) {
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
68
|
+
const s = localStorage.getItem(this.storageKeys.STATE);
|
|
69
|
+
if (!s || s !== t)
|
|
50
70
|
throw new Error("Invalid state parameter - possible CSRF attack");
|
|
51
71
|
await this.exchangeCodeForTokens(e), localStorage.removeItem(this.storageKeys.CODE_VERIFIER), localStorage.removeItem(this.storageKeys.STATE);
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
72
|
+
const r = await this.getAuthState();
|
|
73
|
+
if (r.status !== "authenticated")
|
|
54
74
|
throw new Error("Login failed");
|
|
55
|
-
return this.setAuthState(
|
|
75
|
+
return this.setAuthState(r), r;
|
|
76
|
+
}
|
|
77
|
+
async handleAccessRequestCallback(e) {
|
|
78
|
+
const t = await this.getAccessRequestStatus(e);
|
|
79
|
+
if (!u(t))
|
|
80
|
+
throw l("Failed to get access request status", "auth_error");
|
|
81
|
+
const { status: s, access_request_scope: r } = t.body;
|
|
82
|
+
if (s !== "approved")
|
|
83
|
+
throw l(`Access request is not approved: ${s}`, "auth_error");
|
|
84
|
+
const o = `openid profile email roles ${r ?? ""}`.trim();
|
|
85
|
+
return localStorage.removeItem(this.storageKeys.ACCESS_REQUEST_ID), this.performOAuthPkce(o);
|
|
56
86
|
}
|
|
57
87
|
async logout() {
|
|
58
88
|
const e = localStorage.getItem(this.storageKeys.REFRESH_TOKEN);
|
|
59
89
|
if (e)
|
|
60
90
|
try {
|
|
61
|
-
const
|
|
91
|
+
const s = new URLSearchParams({
|
|
62
92
|
token: e,
|
|
63
93
|
client_id: this.authClientId,
|
|
64
94
|
token_type_hint: "refresh_token"
|
|
@@ -68,12 +98,12 @@ class V extends b {
|
|
|
68
98
|
headers: {
|
|
69
99
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
70
100
|
},
|
|
71
|
-
body:
|
|
101
|
+
body: s
|
|
72
102
|
});
|
|
73
|
-
} catch (
|
|
74
|
-
this.logger.warn("Token revocation failed:",
|
|
103
|
+
} catch (s) {
|
|
104
|
+
this.logger.warn("Token revocation failed:", s);
|
|
75
105
|
}
|
|
76
|
-
localStorage.removeItem(this.storageKeys.ACCESS_TOKEN), localStorage.removeItem(this.storageKeys.REFRESH_TOKEN), localStorage.removeItem(this.storageKeys.EXPIRES_AT)
|
|
106
|
+
localStorage.removeItem(this.storageKeys.ACCESS_TOKEN), localStorage.removeItem(this.storageKeys.REFRESH_TOKEN), localStorage.removeItem(this.storageKeys.EXPIRES_AT);
|
|
77
107
|
const t = {
|
|
78
108
|
status: "unauthenticated",
|
|
79
109
|
user: null,
|
|
@@ -89,7 +119,7 @@ class V extends b {
|
|
|
89
119
|
const t = localStorage.getItem(this.storageKeys.CODE_VERIFIER);
|
|
90
120
|
if (!t)
|
|
91
121
|
throw new Error("Code verifier not found");
|
|
92
|
-
const
|
|
122
|
+
const s = await fetch(this.authEndpoints.token, {
|
|
93
123
|
method: "POST",
|
|
94
124
|
headers: {
|
|
95
125
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
@@ -102,14 +132,14 @@ class V extends b {
|
|
|
102
132
|
code_verifier: t
|
|
103
133
|
})
|
|
104
134
|
});
|
|
105
|
-
if (!
|
|
106
|
-
const
|
|
107
|
-
throw new Error(`Token exchange failed: ${
|
|
135
|
+
if (!s.ok) {
|
|
136
|
+
const o = await s.text();
|
|
137
|
+
throw new Error(`Token exchange failed: ${s.status} ${o}`);
|
|
108
138
|
}
|
|
109
|
-
const
|
|
110
|
-
if (localStorage.setItem(this.storageKeys.ACCESS_TOKEN,
|
|
111
|
-
const
|
|
112
|
-
localStorage.setItem(this.storageKeys.EXPIRES_AT,
|
|
139
|
+
const r = await s.json();
|
|
140
|
+
if (localStorage.setItem(this.storageKeys.ACCESS_TOKEN, r.access_token), r.refresh_token && localStorage.setItem(this.storageKeys.REFRESH_TOKEN, r.refresh_token), r.expires_in) {
|
|
141
|
+
const o = Date.now() + r.expires_in * 1e3;
|
|
142
|
+
localStorage.setItem(this.storageKeys.EXPIRES_AT, o.toString());
|
|
113
143
|
}
|
|
114
144
|
}
|
|
115
145
|
// ============================================================================
|
|
@@ -119,8 +149,8 @@ class V extends b {
|
|
|
119
149
|
return localStorage.getItem(e);
|
|
120
150
|
}
|
|
121
151
|
async _storageSet(e) {
|
|
122
|
-
Object.entries(e).forEach(([t,
|
|
123
|
-
localStorage.setItem(t, String(
|
|
152
|
+
Object.entries(e).forEach(([t, s]) => {
|
|
153
|
+
localStorage.setItem(t, String(s));
|
|
124
154
|
});
|
|
125
155
|
}
|
|
126
156
|
async _storageRemove(e) {
|
|
@@ -130,12 +160,12 @@ class V extends b {
|
|
|
130
160
|
return this.redirectUri;
|
|
131
161
|
}
|
|
132
162
|
}
|
|
133
|
-
const
|
|
134
|
-
class
|
|
135
|
-
constructor(e, t,
|
|
136
|
-
this.state =
|
|
137
|
-
const
|
|
138
|
-
this.storageKeys = K(
|
|
163
|
+
const G = 500, Q = 5e3, Y = 3e4;
|
|
164
|
+
class J {
|
|
165
|
+
constructor(e, t, s) {
|
|
166
|
+
this.state = I, this.bodhiext = null, this.refreshPromise = null, this.logger = new P("WindowBodhiextClient", t.logLevel), this.authClientId = e, this.config = t, this.authEndpoints = q(this.config.authServerUrl), this.onStateChange = s ?? U;
|
|
167
|
+
const r = R(t.basePath, T.WEB_EXT);
|
|
168
|
+
this.storageKeys = K(r), this.apiTimeoutMs = t.apiTimeoutMs ?? Y;
|
|
139
169
|
}
|
|
140
170
|
/**
|
|
141
171
|
* Set client state and notify callback
|
|
@@ -164,7 +194,7 @@ class X {
|
|
|
164
194
|
*/
|
|
165
195
|
ensureBodhiext() {
|
|
166
196
|
if (!this.bodhiext && window.bodhiext && (this.logger.info("Acquiring window.bodhiext reference"), this.bodhiext = window.bodhiext), !this.bodhiext)
|
|
167
|
-
throw
|
|
197
|
+
throw l("Client not initialized", "extension_error");
|
|
168
198
|
}
|
|
169
199
|
/**
|
|
170
200
|
* Send extension request via window.bodhiext.sendExtRequest
|
|
@@ -176,52 +206,52 @@ class X {
|
|
|
176
206
|
* Send API message via window.bodhiext.sendApiRequest
|
|
177
207
|
* Converts ApiResponse to ApiResponseResult
|
|
178
208
|
*/
|
|
179
|
-
async sendApiRequest(e, t,
|
|
209
|
+
async sendApiRequest(e, t, s, r, o) {
|
|
180
210
|
try {
|
|
181
211
|
this.ensureBodhiext();
|
|
182
|
-
} catch (
|
|
212
|
+
} catch (a) {
|
|
183
213
|
return {
|
|
184
214
|
error: {
|
|
185
|
-
message:
|
|
215
|
+
message: a instanceof Error ? a.message : String(a),
|
|
186
216
|
type: "extension_error"
|
|
187
217
|
}
|
|
188
218
|
};
|
|
189
219
|
}
|
|
190
220
|
try {
|
|
191
|
-
const
|
|
192
|
-
(
|
|
193
|
-
() =>
|
|
221
|
+
const a = new Promise(
|
|
222
|
+
(c, i) => setTimeout(
|
|
223
|
+
() => i(
|
|
194
224
|
new Error(
|
|
195
225
|
`[bodhi-js-sdk/web] network timeout: api request not completed within configured/default timeout of ${this.apiTimeoutMs}ms`
|
|
196
226
|
)
|
|
197
227
|
),
|
|
198
228
|
this.apiTimeoutMs
|
|
199
229
|
)
|
|
200
|
-
),
|
|
201
|
-
let
|
|
202
|
-
if (
|
|
203
|
-
const
|
|
204
|
-
if (!
|
|
230
|
+
), n = (async () => {
|
|
231
|
+
let c = r || {};
|
|
232
|
+
if (o) {
|
|
233
|
+
const i = await this._getAccessTokenRaw();
|
|
234
|
+
if (!i)
|
|
205
235
|
return {
|
|
206
236
|
error: {
|
|
207
237
|
message: "Not authenticated. Please log in first.",
|
|
208
238
|
type: "extension_error"
|
|
209
239
|
}
|
|
210
240
|
};
|
|
211
|
-
|
|
212
|
-
...
|
|
213
|
-
Authorization: `Bearer ${
|
|
241
|
+
c = {
|
|
242
|
+
...c,
|
|
243
|
+
Authorization: `Bearer ${i}`
|
|
214
244
|
};
|
|
215
245
|
}
|
|
216
|
-
return this.bodhiext.sendApiRequest(e, t,
|
|
246
|
+
return this.bodhiext.sendApiRequest(e, t, s, c);
|
|
217
247
|
})();
|
|
218
|
-
return await Promise.race([
|
|
219
|
-
} catch (
|
|
220
|
-
const
|
|
248
|
+
return await Promise.race([n, a]);
|
|
249
|
+
} catch (a) {
|
|
250
|
+
const n = a?.error, c = n?.message ?? (a instanceof Error ? a.message : String(a)), i = n?.type || "network_error";
|
|
221
251
|
return {
|
|
222
252
|
error: {
|
|
223
|
-
message:
|
|
224
|
-
type:
|
|
253
|
+
message: c,
|
|
254
|
+
type: i
|
|
225
255
|
}
|
|
226
256
|
};
|
|
227
257
|
}
|
|
@@ -247,30 +277,30 @@ class X {
|
|
|
247
277
|
*/
|
|
248
278
|
async init(e = {}) {
|
|
249
279
|
if (!e.testConnection && !e.selectedConnection)
|
|
250
|
-
return this.logger.info("No testConnection or selectedConnection, returning not-initialized state"),
|
|
280
|
+
return this.logger.info("No testConnection or selectedConnection, returning not-initialized state"), I;
|
|
251
281
|
if (this.bodhiext && !e.testConnection)
|
|
252
282
|
return this.logger.debug("Already have bodhiext handle, skipping polling"), this.state;
|
|
253
283
|
if (!this.bodhiext) {
|
|
254
|
-
const
|
|
255
|
-
if (!await new Promise((
|
|
256
|
-
const
|
|
284
|
+
const r = e.timeoutMs ?? this.config.initParams?.extension?.timeoutMs ?? Q, o = e.intervalMs ?? this.config.initParams?.extension?.intervalMs ?? G, a = Date.now();
|
|
285
|
+
if (!await new Promise((c) => {
|
|
286
|
+
const i = () => {
|
|
257
287
|
if (window.bodhiext) {
|
|
258
|
-
this.bodhiext = window.bodhiext,
|
|
288
|
+
this.bodhiext = window.bodhiext, c(!0);
|
|
259
289
|
return;
|
|
260
290
|
}
|
|
261
|
-
if (Date.now() -
|
|
262
|
-
|
|
291
|
+
if (Date.now() - a >= r) {
|
|
292
|
+
c(!1);
|
|
263
293
|
return;
|
|
264
294
|
}
|
|
265
|
-
setTimeout(
|
|
295
|
+
setTimeout(i, o);
|
|
266
296
|
};
|
|
267
|
-
|
|
297
|
+
i();
|
|
268
298
|
}))
|
|
269
|
-
return this.logger.warn("Extension discovery timed out"), this.setState(
|
|
299
|
+
return this.logger.warn("Extension discovery timed out"), this.setState(L), this.state;
|
|
270
300
|
}
|
|
271
301
|
const t = await this.bodhiext.getExtensionId();
|
|
272
302
|
this.logger.info(`Extension discovered: ${t}`);
|
|
273
|
-
const
|
|
303
|
+
const s = {
|
|
274
304
|
type: "extension",
|
|
275
305
|
extension: "ready",
|
|
276
306
|
extensionId: t,
|
|
@@ -278,83 +308,63 @@ class X {
|
|
|
278
308
|
};
|
|
279
309
|
if (e.testConnection)
|
|
280
310
|
try {
|
|
281
|
-
const
|
|
282
|
-
this.setState({ ...
|
|
283
|
-
} catch (
|
|
284
|
-
this.logger.error("Failed to get server state:",
|
|
311
|
+
const r = await this.getServerState();
|
|
312
|
+
this.setState({ ...s, server: r }), this.logger.info(`Server connectivity tested: ${r.status}`);
|
|
313
|
+
} catch (r) {
|
|
314
|
+
this.logger.error("Failed to get server state:", r), this.setState({ ...s, server: m });
|
|
285
315
|
}
|
|
286
316
|
else
|
|
287
|
-
this.setState(
|
|
317
|
+
this.setState(s);
|
|
288
318
|
return this.state;
|
|
289
319
|
}
|
|
290
320
|
// ============================================================================
|
|
291
321
|
// OAuth Methods
|
|
292
322
|
// ============================================================================
|
|
293
323
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
|
|
297
|
-
async requestResourceAccess(e, t) {
|
|
298
|
-
this.ensureBodhiext();
|
|
299
|
-
const r = {
|
|
300
|
-
app_client_id: this.authClientId,
|
|
301
|
-
...e && { toolset_scope_ids: e },
|
|
302
|
-
...t && { version: t }
|
|
303
|
-
};
|
|
304
|
-
return this.bodhiext.sendApiRequest(
|
|
305
|
-
"POST",
|
|
306
|
-
"/bodhi/v1/apps/request-access",
|
|
307
|
-
r
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Login via browser redirect OAuth2 + PKCE flow
|
|
312
|
-
* @param options - Optional login options (toolsetScopeIds, version)
|
|
313
|
-
* @returns AuthState (though in practice, this redirects and never returns)
|
|
324
|
+
* Login via access-request flow with popup or redirect
|
|
325
|
+
* @param options - Optional login options
|
|
326
|
+
* @returns AuthState
|
|
314
327
|
*/
|
|
315
328
|
async login(e) {
|
|
316
329
|
const t = await this.getAuthState();
|
|
317
330
|
if (t.status === "authenticated")
|
|
318
331
|
return t;
|
|
319
332
|
this.ensureBodhiext();
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (
|
|
324
|
-
const
|
|
325
|
-
|
|
333
|
+
const s = e?.userRole ?? this.config.userRole, r = e?.flowType ?? "popup";
|
|
334
|
+
e?.onProgress?.("requesting");
|
|
335
|
+
const o = new C(this.authClientId).requestedRole(s).flowType(r);
|
|
336
|
+
if (e?.requested && o.requested(e.requested), r === "redirect") {
|
|
337
|
+
const g = e?.redirectUrl ?? this.config.redirectUri;
|
|
338
|
+
o.redirectUrl(g);
|
|
326
339
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
throw c(
|
|
334
|
-
`toolsetScopeIds not received back from request-access call: [${i.join(", ")}], check developer console on configuring the toolset scopes correctly`,
|
|
340
|
+
const a = o.build(), n = await this.requestAccess(a);
|
|
341
|
+
if (y(n))
|
|
342
|
+
throw l(n.error.message, n.error.type);
|
|
343
|
+
if (!u(n))
|
|
344
|
+
throw l(
|
|
345
|
+
`Access request failed: HTTP ${n.status}`,
|
|
335
346
|
"auth_error"
|
|
336
347
|
);
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
return
|
|
357
|
-
});
|
|
348
|
+
const { id: c, review_url: i } = n.body;
|
|
349
|
+
e?.onProgress?.("reviewing");
|
|
350
|
+
let h;
|
|
351
|
+
if (r === "popup") {
|
|
352
|
+
const p = await v(i, async () => {
|
|
353
|
+
const S = await this.getAccessRequestStatus(c);
|
|
354
|
+
if (!u(S)) return null;
|
|
355
|
+
const { status: f, access_request_scope: E } = S.body;
|
|
356
|
+
return f === "approved" ? { approved: !0, accessRequestScope: E ?? void 0 } : ["denied", "failed", "expired"].includes(f) ? { approved: !1 } : null;
|
|
357
|
+
}, {
|
|
358
|
+
intervalMs: e?.pollIntervalMs ?? x,
|
|
359
|
+
timeoutMs: e?.pollTimeoutMs ?? k
|
|
360
|
+
});
|
|
361
|
+
if (!p.approved)
|
|
362
|
+
throw l("Access request was denied or expired", "auth_error");
|
|
363
|
+
h = p.accessRequestScope;
|
|
364
|
+
} else
|
|
365
|
+
return localStorage.setItem(this.storageKeys.ACCESS_REQUEST_ID, c), window.location.href = i, new Promise(() => {
|
|
366
|
+
});
|
|
367
|
+
return e?.onProgress?.("authenticating"), this.performOAuthPkce(`openid profile email roles ${h ?? ""}`.trim());
|
|
358
368
|
}
|
|
359
369
|
/**
|
|
360
370
|
* Handle OAuth callback with authorization code
|
|
@@ -362,14 +372,14 @@ class X {
|
|
|
362
372
|
* @returns AuthState with login state and user info
|
|
363
373
|
*/
|
|
364
374
|
async handleOAuthCallback(e, t) {
|
|
365
|
-
const
|
|
366
|
-
if (!
|
|
375
|
+
const s = localStorage.getItem(this.storageKeys.STATE);
|
|
376
|
+
if (!s || s !== t)
|
|
367
377
|
throw new Error("Invalid state parameter - possible CSRF attack");
|
|
368
378
|
await this.exchangeCodeForTokens(e), localStorage.removeItem(this.storageKeys.CODE_VERIFIER), localStorage.removeItem(this.storageKeys.STATE);
|
|
369
|
-
const
|
|
370
|
-
if (
|
|
379
|
+
const r = await this.getAuthState();
|
|
380
|
+
if (r.status !== "authenticated")
|
|
371
381
|
throw new Error("Login failed");
|
|
372
|
-
return this.setAuthState(
|
|
382
|
+
return this.setAuthState(r), r;
|
|
373
383
|
}
|
|
374
384
|
/**
|
|
375
385
|
* Exchange authorization code for tokens
|
|
@@ -378,29 +388,29 @@ class X {
|
|
|
378
388
|
const t = localStorage.getItem(this.storageKeys.CODE_VERIFIER);
|
|
379
389
|
if (!t)
|
|
380
390
|
throw new Error("Code verifier not found");
|
|
381
|
-
const
|
|
391
|
+
const s = new URLSearchParams({
|
|
382
392
|
grant_type: "authorization_code",
|
|
383
393
|
client_id: this.authClientId,
|
|
384
394
|
code: e,
|
|
385
395
|
redirect_uri: this.config.redirectUri,
|
|
386
396
|
code_verifier: t
|
|
387
|
-
}),
|
|
397
|
+
}), r = await fetch(this.authEndpoints.token, {
|
|
388
398
|
method: "POST",
|
|
389
399
|
headers: {
|
|
390
400
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
391
401
|
},
|
|
392
|
-
body:
|
|
402
|
+
body: s
|
|
393
403
|
});
|
|
394
|
-
if (!
|
|
395
|
-
const
|
|
396
|
-
throw new Error(`Token exchange failed: ${
|
|
404
|
+
if (!r.ok) {
|
|
405
|
+
const a = await r.text();
|
|
406
|
+
throw new Error(`Token exchange failed: ${r.status} ${a}`);
|
|
397
407
|
}
|
|
398
|
-
const
|
|
399
|
-
if (!
|
|
408
|
+
const o = await r.json();
|
|
409
|
+
if (!o.access_token)
|
|
400
410
|
throw new Error("No access token received");
|
|
401
|
-
if (localStorage.setItem(this.storageKeys.ACCESS_TOKEN,
|
|
402
|
-
const
|
|
403
|
-
localStorage.setItem(this.storageKeys.EXPIRES_AT,
|
|
411
|
+
if (localStorage.setItem(this.storageKeys.ACCESS_TOKEN, o.access_token), o.refresh_token && localStorage.setItem(this.storageKeys.REFRESH_TOKEN, o.refresh_token), o.expires_in) {
|
|
412
|
+
const a = Date.now() + o.expires_in * 1e3;
|
|
413
|
+
localStorage.setItem(this.storageKeys.EXPIRES_AT, a.toString());
|
|
404
414
|
}
|
|
405
415
|
}
|
|
406
416
|
/**
|
|
@@ -411,7 +421,7 @@ class X {
|
|
|
411
421
|
const e = localStorage.getItem(this.storageKeys.REFRESH_TOKEN);
|
|
412
422
|
if (e)
|
|
413
423
|
try {
|
|
414
|
-
const
|
|
424
|
+
const s = new URLSearchParams({
|
|
415
425
|
token: e,
|
|
416
426
|
client_id: this.authClientId,
|
|
417
427
|
token_type_hint: "refresh_token"
|
|
@@ -421,12 +431,12 @@ class X {
|
|
|
421
431
|
headers: {
|
|
422
432
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
423
433
|
},
|
|
424
|
-
body:
|
|
434
|
+
body: s
|
|
425
435
|
});
|
|
426
|
-
} catch (
|
|
427
|
-
this.logger.warn("Token revocation failed:",
|
|
436
|
+
} catch (s) {
|
|
437
|
+
this.logger.warn("Token revocation failed:", s);
|
|
428
438
|
}
|
|
429
|
-
localStorage.removeItem(this.storageKeys.ACCESS_TOKEN), localStorage.removeItem(this.storageKeys.REFRESH_TOKEN), localStorage.removeItem(this.storageKeys.EXPIRES_AT), localStorage.removeItem(this.storageKeys.CODE_VERIFIER), localStorage.removeItem(this.storageKeys.STATE)
|
|
439
|
+
localStorage.removeItem(this.storageKeys.ACCESS_TOKEN), localStorage.removeItem(this.storageKeys.REFRESH_TOKEN), localStorage.removeItem(this.storageKeys.EXPIRES_AT), localStorage.removeItem(this.storageKeys.CODE_VERIFIER), localStorage.removeItem(this.storageKeys.STATE);
|
|
430
440
|
const t = {
|
|
431
441
|
status: "unauthenticated",
|
|
432
442
|
user: null,
|
|
@@ -443,7 +453,7 @@ class X {
|
|
|
443
453
|
if (!e)
|
|
444
454
|
return { status: "unauthenticated", user: null, accessToken: null, error: null };
|
|
445
455
|
try {
|
|
446
|
-
return { status: "authenticated", user:
|
|
456
|
+
return { status: "authenticated", user: A(e), accessToken: e, error: null };
|
|
447
457
|
} catch (t) {
|
|
448
458
|
return this.logger.error("Failed to parse token:", t), { status: "unauthenticated", user: null, accessToken: null, error: null };
|
|
449
459
|
}
|
|
@@ -457,10 +467,10 @@ class X {
|
|
|
457
467
|
if (!e)
|
|
458
468
|
return null;
|
|
459
469
|
if (t) {
|
|
460
|
-
const
|
|
461
|
-
if (Date.now() >=
|
|
462
|
-
const
|
|
463
|
-
return
|
|
470
|
+
const s = parseInt(t, 10);
|
|
471
|
+
if (Date.now() >= s - 5 * 1e3) {
|
|
472
|
+
const r = localStorage.getItem(this.storageKeys.REFRESH_TOKEN);
|
|
473
|
+
return r ? this._tryRefreshToken(r) : null;
|
|
464
474
|
}
|
|
465
475
|
}
|
|
466
476
|
return e;
|
|
@@ -485,17 +495,17 @@ class X {
|
|
|
485
495
|
async _doRefreshToken(e) {
|
|
486
496
|
this.logger.debug("Refreshing access token");
|
|
487
497
|
try {
|
|
488
|
-
const t = await
|
|
498
|
+
const t = await D(
|
|
489
499
|
this.authEndpoints.token,
|
|
490
500
|
e,
|
|
491
501
|
this.authClientId
|
|
492
502
|
);
|
|
493
503
|
if (t.success) {
|
|
494
504
|
this._storeRefreshedTokens(t.tokens);
|
|
495
|
-
const
|
|
505
|
+
const s = A(t.tokens.access_token);
|
|
496
506
|
return this.setAuthState({
|
|
497
507
|
status: "authenticated",
|
|
498
|
-
user:
|
|
508
|
+
user: s,
|
|
499
509
|
accessToken: t.tokens.access_token,
|
|
500
510
|
error: null
|
|
501
511
|
}), this.logger.info("Token refreshed successfully"), t.tokens.access_token;
|
|
@@ -510,13 +520,13 @@ class X {
|
|
|
510
520
|
} catch (t) {
|
|
511
521
|
this.logger.warn("Token refresh failed:", t);
|
|
512
522
|
}
|
|
513
|
-
throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"),
|
|
523
|
+
throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"), l(
|
|
514
524
|
"Access token expired and unable to refresh. Try logging out and logging in again.",
|
|
515
525
|
"token_refresh_failed"
|
|
516
526
|
);
|
|
517
527
|
}
|
|
518
528
|
clearAuthStorage() {
|
|
519
|
-
localStorage.removeItem(this.storageKeys.ACCESS_TOKEN), localStorage.removeItem(this.storageKeys.REFRESH_TOKEN), localStorage.removeItem(this.storageKeys.EXPIRES_AT)
|
|
529
|
+
localStorage.removeItem(this.storageKeys.ACCESS_TOKEN), localStorage.removeItem(this.storageKeys.REFRESH_TOKEN), localStorage.removeItem(this.storageKeys.EXPIRES_AT);
|
|
520
530
|
}
|
|
521
531
|
/**
|
|
522
532
|
* Store refreshed tokens
|
|
@@ -537,76 +547,157 @@ class X {
|
|
|
537
547
|
*/
|
|
538
548
|
async getServerState() {
|
|
539
549
|
const e = await this.sendApiRequest("GET", "/bodhi/v1/info");
|
|
540
|
-
if (
|
|
541
|
-
return
|
|
542
|
-
if (!
|
|
543
|
-
return
|
|
544
|
-
const t = e.body;
|
|
550
|
+
if (y(e))
|
|
551
|
+
return m;
|
|
552
|
+
if (!u(e))
|
|
553
|
+
return m;
|
|
554
|
+
const t = e.body, s = t.version || "unknown";
|
|
545
555
|
switch (t.status) {
|
|
546
556
|
case "ready":
|
|
547
|
-
return {
|
|
557
|
+
return {
|
|
558
|
+
status: "ready",
|
|
559
|
+
version: s,
|
|
560
|
+
error: null,
|
|
561
|
+
deployment: t.deployment ?? null,
|
|
562
|
+
client_id: t.client_id ?? null
|
|
563
|
+
};
|
|
548
564
|
case "setup":
|
|
549
|
-
return
|
|
550
|
-
case "
|
|
551
|
-
return
|
|
565
|
+
return w("setup", s, void 0, t.deployment, t.client_id);
|
|
566
|
+
case "resource_admin":
|
|
567
|
+
return w(
|
|
568
|
+
"resource_admin",
|
|
569
|
+
s,
|
|
570
|
+
void 0,
|
|
571
|
+
t.deployment,
|
|
572
|
+
t.client_id
|
|
573
|
+
);
|
|
574
|
+
case "tenant_selection":
|
|
575
|
+
return {
|
|
576
|
+
status: "tenant_selection",
|
|
577
|
+
version: s,
|
|
578
|
+
error: null,
|
|
579
|
+
deployment: t.deployment ?? null,
|
|
580
|
+
client_id: t.client_id ?? null
|
|
581
|
+
};
|
|
552
582
|
case "error":
|
|
553
|
-
return
|
|
583
|
+
return w(
|
|
554
584
|
"error",
|
|
555
|
-
|
|
556
|
-
t.error ? { message: t.error.message, type: t.error.type } :
|
|
585
|
+
s,
|
|
586
|
+
t.error ? { message: t.error.message, type: t.error.type } : M.SERVER_NOT_READY,
|
|
587
|
+
t.deployment,
|
|
588
|
+
t.client_id
|
|
557
589
|
);
|
|
558
590
|
default:
|
|
559
|
-
return
|
|
591
|
+
return m;
|
|
560
592
|
}
|
|
561
593
|
}
|
|
562
594
|
/**
|
|
563
595
|
* Generic streaming via window.bodhiext.sendStreamRequest
|
|
564
596
|
* Wraps ReadableStream as AsyncGenerator
|
|
565
597
|
*/
|
|
566
|
-
async *stream(e, t,
|
|
598
|
+
async *stream(e, t, s, r, o = !0) {
|
|
567
599
|
this.ensureBodhiext();
|
|
568
|
-
let
|
|
569
|
-
if (
|
|
570
|
-
const
|
|
571
|
-
if (!
|
|
572
|
-
throw
|
|
573
|
-
|
|
574
|
-
...
|
|
575
|
-
Authorization: `Bearer ${
|
|
600
|
+
let a = r || {};
|
|
601
|
+
if (o) {
|
|
602
|
+
const i = await this._getAccessTokenRaw();
|
|
603
|
+
if (!i)
|
|
604
|
+
throw l("Not authenticated. Please log in first.", "auth_error");
|
|
605
|
+
a = {
|
|
606
|
+
...a,
|
|
607
|
+
Authorization: `Bearer ${i}`
|
|
576
608
|
};
|
|
577
609
|
}
|
|
578
|
-
const
|
|
610
|
+
const c = this.bodhiext.sendStreamRequest(e, t, s, a).getReader();
|
|
579
611
|
try {
|
|
580
612
|
for (; ; ) {
|
|
581
|
-
const { value:
|
|
582
|
-
if (
|
|
613
|
+
const { value: i, done: h } = await c.read();
|
|
614
|
+
if (h || i?.done)
|
|
583
615
|
break;
|
|
584
|
-
yield
|
|
616
|
+
yield i.body;
|
|
585
617
|
}
|
|
586
|
-
} catch (
|
|
587
|
-
if (
|
|
588
|
-
if ("response" in
|
|
589
|
-
const
|
|
590
|
-
throw F(
|
|
618
|
+
} catch (i) {
|
|
619
|
+
if (i instanceof Error) {
|
|
620
|
+
if ("response" in i) {
|
|
621
|
+
const h = i;
|
|
622
|
+
throw F(i.message, h.response.status, h.response.body);
|
|
591
623
|
}
|
|
592
|
-
throw "error" in
|
|
624
|
+
throw "error" in i ? l(i.message, "extension_error") : l(i.message, "extension_error");
|
|
593
625
|
}
|
|
594
|
-
throw
|
|
626
|
+
throw i;
|
|
595
627
|
} finally {
|
|
596
|
-
|
|
628
|
+
c.releaseLock();
|
|
597
629
|
}
|
|
598
630
|
}
|
|
599
631
|
// ============================================================================
|
|
600
632
|
// OpenAI-Compatible Namespaced API
|
|
601
633
|
// ============================================================================
|
|
602
634
|
get chat() {
|
|
603
|
-
return this._chat ??= new
|
|
635
|
+
return this._chat ??= new B(this);
|
|
604
636
|
}
|
|
605
637
|
get models() {
|
|
606
|
-
return this._models ??= new
|
|
638
|
+
return this._models ??= new $(this);
|
|
607
639
|
}
|
|
608
640
|
get embeddings() {
|
|
609
|
-
return this._embeddings ??= new
|
|
641
|
+
return this._embeddings ??= new V(this);
|
|
642
|
+
}
|
|
643
|
+
get toolsets() {
|
|
644
|
+
return this._toolsets ??= new H(this);
|
|
645
|
+
}
|
|
646
|
+
get mcps() {
|
|
647
|
+
return this._mcps ??= new z(this);
|
|
648
|
+
}
|
|
649
|
+
// ============================================================================
|
|
650
|
+
// Access Request Methods
|
|
651
|
+
// ============================================================================
|
|
652
|
+
async requestAccess(e) {
|
|
653
|
+
return this.sendApiRequest(
|
|
654
|
+
"POST",
|
|
655
|
+
"/bodhi/v1/apps/request-access",
|
|
656
|
+
e,
|
|
657
|
+
{},
|
|
658
|
+
!1
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
async getAccessRequestStatus(e) {
|
|
662
|
+
return this.sendApiRequest(
|
|
663
|
+
"GET",
|
|
664
|
+
`/bodhi/v1/apps/access-requests/${e}?app_client_id=${encodeURIComponent(this.authClientId)}`,
|
|
665
|
+
void 0,
|
|
666
|
+
{},
|
|
667
|
+
!1
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
async pollAccessRequestStatus(e, t) {
|
|
671
|
+
return W(
|
|
672
|
+
(s) => this.getAccessRequestStatus(s),
|
|
673
|
+
e,
|
|
674
|
+
t
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
async performOAuthPkce(e) {
|
|
678
|
+
const t = _(), s = await b(t), r = _();
|
|
679
|
+
localStorage.setItem(this.storageKeys.CODE_VERIFIER, t), localStorage.setItem(this.storageKeys.STATE, r);
|
|
680
|
+
const o = e.split(" ").filter(Boolean), a = new URLSearchParams({
|
|
681
|
+
response_type: "code",
|
|
682
|
+
client_id: this.authClientId,
|
|
683
|
+
redirect_uri: this.config.redirectUri,
|
|
684
|
+
scope: o.join(" "),
|
|
685
|
+
state: r,
|
|
686
|
+
code_challenge: s,
|
|
687
|
+
code_challenge_method: "S256"
|
|
688
|
+
});
|
|
689
|
+
return window.location.href = `${this.authEndpoints.authorize}?${a}`, new Promise(() => {
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
async handleAccessRequestCallback(e) {
|
|
693
|
+
const t = await this.getAccessRequestStatus(e);
|
|
694
|
+
if (!u(t))
|
|
695
|
+
throw l("Failed to get access request status", "auth_error");
|
|
696
|
+
const { status: s, access_request_scope: r } = t.body;
|
|
697
|
+
if (s !== "approved")
|
|
698
|
+
throw l(`Access request is not approved: ${s}`, "auth_error");
|
|
699
|
+
const o = `openid profile email roles ${r ?? ""}`.trim();
|
|
700
|
+
return localStorage.removeItem(this.storageKeys.ACCESS_REQUEST_ID), this.performOAuthPkce(o);
|
|
610
701
|
}
|
|
611
702
|
/**
|
|
612
703
|
* Serialize web extension client state (all transient, nothing to persist)
|
|
@@ -628,42 +719,42 @@ class X {
|
|
|
628
719
|
authClientId: this.authClientId,
|
|
629
720
|
authServerUrl: this.config.authServerUrl,
|
|
630
721
|
redirectUri: this.config.redirectUri,
|
|
631
|
-
|
|
722
|
+
userRole: this.config.userRole
|
|
632
723
|
};
|
|
633
724
|
}
|
|
634
725
|
}
|
|
635
|
-
function
|
|
726
|
+
function Z(d) {
|
|
636
727
|
if (typeof window > "u")
|
|
637
728
|
throw new Error("redirectUri required in non-browser environment");
|
|
638
729
|
const e = d === "/" ? "" : d.replace(/\/$/, "");
|
|
639
730
|
return `${window.location.origin}${e}/callback`;
|
|
640
731
|
}
|
|
641
|
-
class
|
|
642
|
-
constructor(e, t,
|
|
643
|
-
const
|
|
644
|
-
basePath:
|
|
645
|
-
redirectUri:
|
|
646
|
-
authServerUrl:
|
|
647
|
-
|
|
648
|
-
logLevel:
|
|
649
|
-
apiTimeoutMs:
|
|
650
|
-
initParams:
|
|
732
|
+
class te extends X {
|
|
733
|
+
constructor(e, t, s) {
|
|
734
|
+
const r = t || {}, o = {
|
|
735
|
+
basePath: r.basePath || "/",
|
|
736
|
+
redirectUri: r.redirectUri || Z(r.basePath || "/"),
|
|
737
|
+
authServerUrl: r.authServerUrl || "https://id.getbodhi.app/realms/bodhi",
|
|
738
|
+
userRole: r.userRole || "scope_user_user",
|
|
739
|
+
logLevel: r.logLevel || "warn",
|
|
740
|
+
apiTimeoutMs: r.apiTimeoutMs,
|
|
741
|
+
initParams: r.initParams
|
|
651
742
|
};
|
|
652
|
-
super(e,
|
|
743
|
+
super(e, o, s);
|
|
653
744
|
}
|
|
654
745
|
createLogger(e) {
|
|
655
|
-
return new
|
|
746
|
+
return new P("WebUIClient", e.logLevel);
|
|
656
747
|
}
|
|
657
748
|
createStoragePrefix(e) {
|
|
658
|
-
return
|
|
749
|
+
return R(e.basePath, T.WEB);
|
|
659
750
|
}
|
|
660
751
|
createExtClient(e, t) {
|
|
661
|
-
return new
|
|
752
|
+
return new J(
|
|
662
753
|
this.authClientId,
|
|
663
754
|
{
|
|
664
755
|
authServerUrl: e.authServerUrl,
|
|
665
756
|
redirectUri: e.redirectUri,
|
|
666
|
-
|
|
757
|
+
userRole: e.userRole,
|
|
667
758
|
basePath: e.basePath,
|
|
668
759
|
logLevel: e.logLevel,
|
|
669
760
|
apiTimeoutMs: e.apiTimeoutMs,
|
|
@@ -672,18 +763,18 @@ class Y extends q {
|
|
|
672
763
|
t
|
|
673
764
|
);
|
|
674
765
|
}
|
|
675
|
-
createDirectClient(e, t,
|
|
676
|
-
return new
|
|
766
|
+
createDirectClient(e, t, s) {
|
|
767
|
+
return new j(
|
|
677
768
|
{
|
|
678
769
|
authClientId: e,
|
|
679
770
|
authServerUrl: t.authServerUrl,
|
|
680
771
|
redirectUri: t.redirectUri,
|
|
681
|
-
|
|
772
|
+
userRole: t.userRole,
|
|
682
773
|
logLevel: t.logLevel,
|
|
683
774
|
basePath: t.basePath,
|
|
684
775
|
apiTimeoutMs: t.apiTimeoutMs
|
|
685
776
|
},
|
|
686
|
-
|
|
777
|
+
s
|
|
687
778
|
);
|
|
688
779
|
}
|
|
689
780
|
// ============================================================================
|
|
@@ -692,9 +783,12 @@ class Y extends q {
|
|
|
692
783
|
async handleOAuthCallback(e, t) {
|
|
693
784
|
return this.connectionMode === "direct" ? this.directClient.handleOAuthCallback(e, t) : this.extClient.handleOAuthCallback(e, t);
|
|
694
785
|
}
|
|
786
|
+
async handleAccessRequestCallback(e) {
|
|
787
|
+
return this.connectionMode === "direct" ? this.directClient.handleAccessRequestCallback(e) : this.extClient.handleAccessRequestCallback(e);
|
|
788
|
+
}
|
|
695
789
|
}
|
|
696
|
-
const
|
|
790
|
+
const se = "production";
|
|
697
791
|
export {
|
|
698
|
-
|
|
699
|
-
|
|
792
|
+
se as WEB_BUILD_MODE,
|
|
793
|
+
te as WebUIClient
|
|
700
794
|
};
|
package/dist/direct-client.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { DirectClientBase, AuthState, LoginOptions, LogLevel, StateChangeCallback } from '@bodhiapp/bodhi-js-core';
|
|
2
|
+
import { UserScope } from '@bodhiapp/ts-client';
|
|
2
3
|
/**
|
|
3
4
|
* Configuration for DirectWebClient
|
|
4
5
|
*/
|
|
5
6
|
export interface DirectWebClientConfig {
|
|
6
7
|
authClientId: string;
|
|
7
8
|
authServerUrl: string;
|
|
8
|
-
|
|
9
|
+
userRole: UserScope;
|
|
9
10
|
basePath: string;
|
|
10
11
|
logLevel: LogLevel;
|
|
11
12
|
redirectUri: string;
|
|
@@ -18,7 +19,9 @@ export declare class DirectWebClient extends DirectClientBase {
|
|
|
18
19
|
private redirectUri;
|
|
19
20
|
constructor(config: DirectWebClientConfig, onStateChange?: StateChangeCallback);
|
|
20
21
|
login(options?: LoginOptions): Promise<AuthState>;
|
|
22
|
+
protected performOAuthPkce(scope: string): Promise<AuthState>;
|
|
21
23
|
handleOAuthCallback(code: string, state: string): Promise<AuthState>;
|
|
24
|
+
handleAccessRequestCallback(requestId: string): Promise<AuthState>;
|
|
22
25
|
logout(): Promise<AuthState>;
|
|
23
26
|
protected exchangeCodeForTokens(code: string): Promise<void>;
|
|
24
27
|
protected _storageGet(key: string): Promise<string | null>;
|
package/dist/ext-client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AccessRequestStatusResponse, CreateAccessRequest, CreateAccessRequestResponse, UserScope } from '@bodhiapp/ts-client';
|
|
2
|
+
import { Chat, Models, Embeddings, Toolsets, Mcps, ApiResponseResult, AuthState, BackendServerState, ClientState, ExtensionState, IExtensionClient, InitParams, LoginOptions, LogLevel, StateChangeCallback } from '@bodhiapp/bodhi-js-core';
|
|
2
3
|
export type SerializedWebExtensionState = {
|
|
3
4
|
extensionId?: string;
|
|
4
5
|
};
|
|
@@ -9,7 +10,7 @@ export type SerializedWebExtensionState = {
|
|
|
9
10
|
export interface WindowBodhiextClientConfig {
|
|
10
11
|
authServerUrl: string;
|
|
11
12
|
redirectUri: string;
|
|
12
|
-
|
|
13
|
+
userRole: UserScope;
|
|
13
14
|
basePath: string;
|
|
14
15
|
logLevel: LogLevel;
|
|
15
16
|
apiTimeoutMs?: number;
|
|
@@ -43,6 +44,8 @@ export declare class WindowBodhiextClient implements IExtensionClient {
|
|
|
43
44
|
private _chat;
|
|
44
45
|
private _models;
|
|
45
46
|
private _embeddings;
|
|
47
|
+
private _toolsets;
|
|
48
|
+
private _mcps;
|
|
46
49
|
constructor(authClientId: string, config: WindowBodhiextClientConfig, onStateChange?: StateChangeCallback);
|
|
47
50
|
/**
|
|
48
51
|
* Set client state and notify callback
|
|
@@ -85,14 +88,9 @@ export declare class WindowBodhiextClient implements IExtensionClient {
|
|
|
85
88
|
*/
|
|
86
89
|
init(params?: InitParams): Promise<ExtensionState>;
|
|
87
90
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
|
|
91
|
-
private requestResourceAccess;
|
|
92
|
-
/**
|
|
93
|
-
* Login via browser redirect OAuth2 + PKCE flow
|
|
94
|
-
* @param options - Optional login options (toolsetScopeIds, version)
|
|
95
|
-
* @returns AuthState (though in practice, this redirects and never returns)
|
|
91
|
+
* Login via access-request flow with popup or redirect
|
|
92
|
+
* @param options - Optional login options
|
|
93
|
+
* @returns AuthState
|
|
96
94
|
*/
|
|
97
95
|
login(options?: LoginOptions): Promise<AuthState>;
|
|
98
96
|
/**
|
|
@@ -152,6 +150,16 @@ export declare class WindowBodhiextClient implements IExtensionClient {
|
|
|
152
150
|
get chat(): Chat;
|
|
153
151
|
get models(): Models;
|
|
154
152
|
get embeddings(): Embeddings;
|
|
153
|
+
get toolsets(): Toolsets;
|
|
154
|
+
get mcps(): Mcps;
|
|
155
|
+
requestAccess(body: CreateAccessRequest): Promise<ApiResponseResult<CreateAccessRequestResponse>>;
|
|
156
|
+
getAccessRequestStatus(requestId: string): Promise<ApiResponseResult<AccessRequestStatusResponse>>;
|
|
157
|
+
pollAccessRequestStatus(requestId: string, options?: {
|
|
158
|
+
intervalMs?: number;
|
|
159
|
+
timeoutMs?: number;
|
|
160
|
+
}): Promise<AccessRequestStatusResponse>;
|
|
161
|
+
private performOAuthPkce;
|
|
162
|
+
handleAccessRequestCallback(requestId: string): Promise<AuthState>;
|
|
155
163
|
/**
|
|
156
164
|
* Serialize web extension client state (all transient, nothing to persist)
|
|
157
165
|
*/
|
package/dist/facade-client.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { WindowBodhiextClient } from './ext-client';
|
|
|
8
8
|
export interface WebClientConfig {
|
|
9
9
|
authServerUrl: string;
|
|
10
10
|
redirectUri: string;
|
|
11
|
-
|
|
11
|
+
userRole: UserScope;
|
|
12
12
|
basePath: string;
|
|
13
13
|
logLevel: LogLevel;
|
|
14
14
|
apiTimeoutMs?: number;
|
|
@@ -26,7 +26,7 @@ export interface WebClientConfig {
|
|
|
26
26
|
export interface WebUIClientParams {
|
|
27
27
|
redirectUri?: string;
|
|
28
28
|
authServerUrl?: string;
|
|
29
|
-
|
|
29
|
+
userRole?: UserScope;
|
|
30
30
|
basePath?: string;
|
|
31
31
|
logLevel?: LogLevel;
|
|
32
32
|
apiTimeoutMs?: number;
|
|
@@ -50,4 +50,5 @@ export declare class WebUIClient extends BaseFacadeClient<WebClientConfig, Windo
|
|
|
50
50
|
protected createExtClient(config: WebClientConfig, onStateChange: (change: StateChange) => void): WindowBodhiextClient;
|
|
51
51
|
protected createDirectClient(authClientId: string, config: WebClientConfig, onStateChange: (change: StateChange) => void): DirectWebClient;
|
|
52
52
|
handleOAuthCallback(code: string, state: string): Promise<AuthState>;
|
|
53
|
+
handleAccessRequestCallback(requestId: string): Promise<AuthState>;
|
|
53
54
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bodhiapp/bodhi-js",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"description": "Web SDK for Bodhi Browser - window.bodhiext communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/bodhi-web.cjs.js",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"typecheck": "tsc --noEmit"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@bodhiapp/bodhi-browser-types": "0.0.
|
|
41
|
-
"@bodhiapp/bodhi-js-core": "0.0.
|
|
42
|
-
"@bodhiapp/ts-client": "0.1.
|
|
40
|
+
"@bodhiapp/bodhi-browser-types": "0.0.24",
|
|
41
|
+
"@bodhiapp/bodhi-js-core": "0.0.24",
|
|
42
|
+
"@bodhiapp/ts-client": "0.1.19"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@eslint/js": "^9.23.0",
|