@bodhiapp/bodhi-js 0.0.37 → 0.0.38

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.
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("@bodhiapp/bodhi-js-core");class S{async get(e){return localStorage.getItem(e)}async set(e){Object.entries(e).forEach(([t,i])=>{localStorage.setItem(t,String(i))})}async remove(e){e.forEach(t=>localStorage.removeItem(t))}}class w extends r.DirectClientBase{constructor(e,t){const i=r.createStoragePrefixWithBasePath(e.basePath,r.STORAGE_PREFIXES.WEB_DIRECT),s={authClientId:e.authClientId,authServerUrl:e.authServerUrl,storagePrefix:i,logLevel:e.logLevel,loggerPrefix:"DirectWebClient",apiTimeoutMs:e.apiTimeoutMs,storage:e.storage??new S,initialTokens:e.initialTokens};super(s,t),this.redirectUri=e.redirectUri}async login(e){const t=await this.getAuthState();if(t.status==="authenticated")return t;const i=e?.userRole??"scope_user_user",s=e?.flowType??"popup";e?.onProgress?.("requesting");const o=new r.AccessRequestBuilder(this.authClientId).requestedRole(i).flowType(s);if(e?.requested&&o.requested(e.requested),s==="redirect"){const u=e?.redirectUrl??this.redirectUri;o.redirectUrl(u)}const n=o.build(),c=await this.requestAccess(n),{id:l,review_url:a}=r.unwrapResponse(c);e?.onProgress?.("reviewing");let h;if(s==="popup"){const u=async()=>{const E=await this.getAccessRequestStatus(l);if(E.status>=400)return null;const{status:g,access_request_scope:p}=E.body;return g==="approved"?{approved:!0,accessRequestScope:p??void 0}:["denied","failed","expired"].includes(g)?{approved:!1,status:g}:null},d=await r.openPopupReview(a,u,{intervalMs:e?.pollIntervalMs??r.DEFAULT_POLL_INTERVAL_MS,timeoutMs:e?.pollTimeoutMs??r.DEFAULT_POLL_TIMEOUT_MS});d.approved||r.throwAccessRequestDenialError(d.status??"unknown"),h=d.accessRequestScope}else return await this._storageSet({[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(),i=await r.generateCodeChallenge(t),s=r.generateCodeVerifier();await this._storageSet({[this.storageKeys.CODE_VERIFIER]:t,[this.storageKeys.STATE]:s});const o=new URL(this.authEndpoints.authorize);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",i),o.searchParams.set("code_challenge_method","S256"),o.searchParams.set("state",s),window.location.href=o.toString(),new Error("Redirect initiated")}async handleOAuthCallback(e,t){const i=await this._storageGet(this.storageKeys.STATE);if(!i||i!==t)throw new Error("Invalid state parameter - possible CSRF attack");await this.exchangeCodeForTokens(e);const s=await this.getAuthState();if(s.status!=="authenticated")throw new Error("Login failed");return this.setAuthState(s),s}async handleAccessRequestCallback(e){const t=await this.getAccessRequestStatus(e),{status:i,access_request_scope:s}=r.unwrapResponse(t);await this._storageRemove([this.storageKeys.ACCESS_REQUEST_ID]),i!=="approved"&&r.throwAccessRequestDenialError(i);const o=`openid profile email roles ${s??""}`.trim();return this.performOAuthPkce(o)}_getRedirectUri(){return this.redirectUri}}const _=500,m=5e3,T=3e4;class R{constructor(e,t,i){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=i??r.NOOP_STATE_CALLBACK;const s=r.createStoragePrefixWithBasePath(t.basePath,r.STORAGE_PREFIXES.WEB_EXT);this.storageKeys=r.createStorageKeys(s),this.apiTimeoutMs=t.apiTimeoutMs??T}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("not_initialized","Client not initialized")}async sendExtRequest(e,t){return this.ensureBodhiext(),this.bodhiext.sendExtRequest(e,t)}async sendApiRequest(e,t,i,s,o){this.ensureBodhiext();try{const n=new Promise((l,a)=>setTimeout(()=>a(new r.BodhiError("timeout_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=s||{};if(o){const a=await this._getAccessTokenRaw();if(!a)throw new r.BodhiError("auth_error","Not authenticated. Please log in first.");l={...l,Authorization:`Bearer ${a}`}}return this.bodhiext.sendApiRequest(e,t,i,l)})();return await Promise.race([c,n])}catch(n){if(n instanceof r.BodhiApiError||n instanceof r.BodhiError)throw n;if(n instanceof Error){const c=n;throw n.name==="BodhiApiError"&&typeof c.status=="number"&&c.body!=null?new r.BodhiApiError(c.status,c.body,n.message,c.headers):n.name==="BodhiError"&&typeof c.code=="string"?new r.BodhiError(c.code,n.message):new r.BodhiError("network_error",n.message)}throw new r.BodhiError("network_error",String(n))}}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 s=e.timeoutMs??this.config.initParams?.extension?.timeoutMs??m,o=e.intervalMs??this.config.initParams?.extension?.intervalMs??_,n=Date.now();if(!await new Promise(l=>{const a=()=>{if(window.bodhiext){this.bodhiext=window.bodhiext,l(!0);return}if(Date.now()-n>=s){l(!1);return}setTimeout(a,o)};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 i={type:"extension",extension:"ready",extensionId:t,server:r.PENDING_EXTENSION_READY};if(e.testConnection)try{const s=await this.getServerState();this.setState({...i,server:s}),this.logger.info(`Server connectivity tested: ${s.status}`)}catch(s){this.logger.error("Failed to get server state:",s),this.setState({...i,server:r.BACKEND_SERVER_NOT_REACHABLE})}else this.setState(i);return this.state}async login(e){const t=await this.getAuthState();if(t.status==="authenticated")return t;this.ensureBodhiext();const i=e?.userRole??"scope_user_user",s=e?.flowType??"popup";e?.onProgress?.("requesting");const o=new r.AccessRequestBuilder(this.authClientId).requestedRole(i).flowType(s);if(e?.requested&&o.requested(e.requested),s==="redirect"){const u=e?.redirectUrl??this.config.redirectUri;o.redirectUrl(u)}const n=o.build(),c=await this.requestAccess(n),{id:l,review_url:a}=r.unwrapResponse(c);e?.onProgress?.("reviewing");let h;if(s==="popup"){const u=async()=>{const E=await this.getAccessRequestStatus(l);if(E.status>=400)return null;const{status:g,access_request_scope:p}=E.body;return g==="approved"?{approved:!0,accessRequestScope:p??void 0}:["denied","failed","expired"].includes(g)?{approved:!1,status:g}:null},d=await r.openPopupReview(a,u,{intervalMs:e?.pollIntervalMs??r.DEFAULT_POLL_INTERVAL_MS,timeoutMs:e?.pollTimeoutMs??r.DEFAULT_POLL_TIMEOUT_MS});d.approved||r.throwAccessRequestDenialError(d.status??"unknown"),h=d.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 i=localStorage.getItem(this.storageKeys.STATE);if(!i||i!==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 s=await this.getAuthState();if(s.status!=="authenticated")throw new Error("Login failed");return this.setAuthState(s),s}async exchangeCodeForTokens(e){const t=localStorage.getItem(this.storageKeys.CODE_VERIFIER);if(!t)throw new Error("Code verifier not found");const i=new URLSearchParams({grant_type:"authorization_code",client_id:this.authClientId,code:e,redirect_uri:this.config.redirectUri,code_verifier:t}),s=await fetch(this.authEndpoints.token,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:i});if(!s.ok){const n=await s.text();throw new Error(`Token exchange failed: ${s.status} ${n}`)}const o=await s.json();if(!o.access_token)throw new Error("No access token received");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 logout(){const e=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);if(e)try{const i=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:i})}catch(i){this.logger.warn("Token revocation failed:",i)}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,refreshToken:null,expiresAt:null,isTokenRefresh:!1};return this.setAuthState(t),t}async getAuthState(){const e=await this._getAccessTokenRaw();if(!e)return{status:"unauthenticated",user:null,accessToken:null,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1};try{return{status:"authenticated",user:r.extractUserInfo(e),accessToken:e,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1}}catch(t){return this.logger.error("Failed to parse token:",t),{status:"unauthenticated",user:null,accessToken:null,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1}}}async _getAccessTokenRaw(){const e=localStorage.getItem(this.storageKeys.ACCESS_TOKEN),t=localStorage.getItem(this.storageKeys.EXPIRES_AT);if(!e)return null;if(t){const i=parseInt(t,10);if(Date.now()>=i-5*1e3){const s=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);return s?this._tryRefreshToken(s):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 i=r.extractUserInfo(t.tokens.access_token);return this.setAuthState({status:"authenticated",user:i,accessToken:t.tokens.access_token,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!0}),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,refreshToken:null,expiresAt:null,isTokenRefresh:!1}),null}catch(t){this.logger.warn("Token refresh failed:",t)}throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"),r.createOperationError("auth_error","Access token expired and unable to refresh. Try logging out and logging in again.")}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(){try{const e=await this.sendApiRequest("GET","/bodhi/v1/info");if(e.status>=400)return r.BACKEND_SERVER_NOT_REACHABLE;const t=e.body,i=t.version||"unknown";switch(t.status){case"ready":return{status:"ready",version:i,error:null,deployment:t.deployment??null,client_id:t.client_id??null};case"setup":return r.backendServerNotReady("setup",i,void 0,t.deployment,t.client_id);case"resource_admin":return r.backendServerNotReady("resource_admin",i,void 0,t.deployment,t.client_id);case"error":return r.backendServerNotReady("error",i,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}}catch{return r.BACKEND_SERVER_NOT_REACHABLE}}async*stream(e,t,i,s,o=!0){this.ensureBodhiext();let n=s||{};if(o){const a=await this._getAccessTokenRaw();if(!a)throw r.createOperationError("auth_error","Not authenticated. Please log in first.");n={...n,Authorization:`Bearer ${a}`}}const l=this.bodhiext.sendStreamRequest(e,t,i,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 r.BodhiApiError||a instanceof r.BodhiError)throw a;if(a instanceof Error){const h=a;throw a.name==="BodhiApiError"&&typeof h.status=="number"&&h.body!=null?new r.BodhiApiError(h.status,h.body,a.message,h.headers):a.name==="BodhiError"&&typeof h.code=="string"?new r.BodhiError(h.code,a.message):new r.BodhiError("extension_error",a.message)}throw a}finally{l.releaseLock()}}async streamText(e,t,i,s,o=!0){this.ensureBodhiext();let n={...s};if(o){const a=await this._getAccessTokenRaw();a&&(n={...n,Authorization:`Bearer ${a}`})}const c=await this.bodhiext.sendStreamText(e,t,i,n);async function*l(a){const h=a.getReader();try{for(;;){const{done:u,value:d}=await h.read();if(u)break;yield d}}finally{h.releaseLock()}}return{status:c.status,headers:c.headers,body:l(c.body)}}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 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(i=>this.getAccessRequestStatus(i),e,t)}async performOAuthPkce(e){const t=r.generateCodeVerifier(),i=await r.generateCodeChallenge(t),s=r.generateCodeVerifier();localStorage.setItem(this.storageKeys.CODE_VERIFIER,t),localStorage.setItem(this.storageKeys.STATE,s);const o=e.split(" ").filter(Boolean),n=new URLSearchParams({response_type:"code",client_id:this.authClientId,redirect_uri:this.config.redirectUri,scope:o.join(" "),state:s,code_challenge:i,code_challenge_method:"S256"});return window.location.href=`${this.authEndpoints.authorize}?${n}`,new Promise(()=>{})}async handleAccessRequestCallback(e){const t=await this.getAccessRequestStatus(e),{status:i,access_request_scope:s}=r.unwrapResponse(t);localStorage.removeItem(this.storageKeys.ACCESS_REQUEST_ID),i!=="approved"&&r.throwAccessRequestDenialError(i);const o=`openid profile email roles ${s??""}`.trim();return this.performOAuthPkce(o)}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}}}function y(f){if(typeof window>"u")throw new Error("redirectUri required in non-browser environment");const e=f==="/"?"":f.replace(/\/$/,"");return`${window.location.origin}${e}/callback`}class A extends r.BaseFacadeClient{constructor(e,t,i){const s=t||{},o={basePath:s.basePath||"/",redirectUri:s.redirectUri||y(s.basePath||"/"),authServerUrl:s.authServerUrl||"https://id.getbodhi.app/realms/bodhi",logLevel:s.logLevel||"warn",apiTimeoutMs:s.apiTimeoutMs,storage:s.storage,initialTokens:s.initialTokens,initParams:s.initParams};super(e,o,i)}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 R(this.authClientId,{authServerUrl:e.authServerUrl,redirectUri:e.redirectUri,basePath:e.basePath,logLevel:e.logLevel,apiTimeoutMs:e.apiTimeoutMs,initParams:e.initParams},t)}createDirectClient(e,t,i){return new w({authClientId:e,authServerUrl:t.authServerUrl,redirectUri:t.redirectUri,logLevel:t.logLevel,basePath:t.basePath,apiTimeoutMs:t.apiTimeoutMs,storage:t.storage,initialTokens:t.initialTokens},i)}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 k="production";Object.defineProperty(exports,"InMemoryStorage",{enumerable:!0,get:()=>r.InMemoryStorage});exports.DirectWebClient=w;exports.LocalStorageAdapter=S;exports.WEB_BUILD_MODE=k;exports.WebUIClient=A;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("@bodhiapp/bodhi-js-core");class S{async get(e){return localStorage.getItem(e)}async set(e){Object.entries(e).forEach(([t,i])=>{localStorage.setItem(t,String(i))})}async remove(e){e.forEach(t=>localStorage.removeItem(t))}}class w extends r.DirectClientBase{constructor(e,t){const i=r.createStoragePrefixWithNamespace(e.basePath,r.STORAGE_PREFIXES.WEB_DIRECT),s={authClientId:e.authClientId,authServerUrl:e.authServerUrl,storagePrefix:i,logLevel:e.logLevel,loggerPrefix:"DirectWebClient",apiTimeoutMs:e.apiTimeoutMs,storage:e.storage??new S,initialTokens:e.initialTokens};super(s,t),this.redirectUri=e.redirectUri}async login(e){const t=await this.getAuthState();if(t.status==="authenticated")return t;const i=e?.userRole??"scope_user_user",s=e?.flowType??"popup";e?.onProgress?.("requesting");const o=new r.AccessRequestBuilder(this.authClientId).requestedRole(i).flowType(s);if(e?.requested&&o.requested(e.requested),s==="redirect"){const u=e?.redirectUrl??this.redirectUri;o.redirectUrl(u)}const n=o.build(),c=await this.requestAccess(n),{id:l,review_url:a}=r.unwrapResponse(c);e?.onProgress?.("reviewing");let h;if(s==="popup"){const u=async()=>{const p=await this.getAccessRequestStatus(l);if(p.status>=400)return null;const{status:g,access_request_scope:E}=p.body;return g==="approved"?{approved:!0,accessRequestScope:E??void 0}:["denied","failed","expired"].includes(g)?{approved:!1,status:g}:null},d=await r.openPopupReview(a,u,{intervalMs:e?.pollIntervalMs??r.DEFAULT_POLL_INTERVAL_MS,timeoutMs:e?.pollTimeoutMs??r.DEFAULT_POLL_TIMEOUT_MS});d.approved||r.throwAccessRequestDenialError(d.status??"unknown"),h=d.accessRequestScope}else return await this._storageSet({[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(),i=await r.generateCodeChallenge(t),s=r.generateCodeVerifier();await this._storageSet({[this.storageKeys.CODE_VERIFIER]:t,[this.storageKeys.STATE]:s});const o=new URL(this.authEndpoints.authorize);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",i),o.searchParams.set("code_challenge_method","S256"),o.searchParams.set("state",s),window.location.href=o.toString(),new Error("Redirect initiated")}async handleOAuthCallback(e,t){const i=await this._storageGet(this.storageKeys.STATE);if(!i||i!==t)throw new Error("Invalid state parameter - possible CSRF attack");await this.exchangeCodeForTokens(e);const s=await this.getAuthState();if(s.status!=="authenticated")throw new Error("Login failed");return this.setAuthState(s),s}async handleAccessRequestCallback(e){const t=await this.getAccessRequestStatus(e),{status:i,access_request_scope:s}=r.unwrapResponse(t);await this._storageRemove([this.storageKeys.ACCESS_REQUEST_ID]),i!=="approved"&&r.throwAccessRequestDenialError(i);const o=`openid profile email roles ${s??""}`.trim();return this.performOAuthPkce(o)}_getRedirectUri(){return this.redirectUri}}const m=500,_=5e3,T=3e4;class R{constructor(e,t,i){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=i??r.NOOP_STATE_CALLBACK;const s=r.createStoragePrefixWithNamespace(t.basePath,r.STORAGE_PREFIXES.WEB_EXT);this.storageKeys=r.createStorageKeys(s),this.apiTimeoutMs=t.apiTimeoutMs??T}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("not_initialized","Client not initialized")}async sendExtRequest(e,t){return this.ensureBodhiext(),this.bodhiext.sendExtRequest(e,t)}async sendApiRequest(e,t,i,s,o){this.ensureBodhiext();try{const n=new Promise((l,a)=>setTimeout(()=>a(new r.BodhiError("timeout_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=s||{};if(o){const a=await this._getAccessTokenRaw();if(!a)throw new r.BodhiError("auth_error","Not authenticated. Please log in first.");l={...l,Authorization:`Bearer ${a}`}}return this.bodhiext.sendApiRequest(e,t,i,l)})();return await Promise.race([c,n])}catch(n){if(n instanceof r.BodhiApiError||n instanceof r.BodhiError)throw n;if(n instanceof Error){const c=n;throw n.name==="BodhiApiError"&&typeof c.status=="number"&&c.body!=null?new r.BodhiApiError(c.status,c.body,n.message,c.headers):n.name==="BodhiError"&&typeof c.code=="string"?new r.BodhiError(c.code,n.message):new r.BodhiError("network_error",n.message)}throw new r.BodhiError("network_error",String(n))}}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 s=e.timeoutMs??this.config.initParams?.extension?.timeoutMs??_,o=e.intervalMs??this.config.initParams?.extension?.intervalMs??m,n=Date.now();if(!await new Promise(l=>{const a=()=>{if(window.bodhiext){this.bodhiext=window.bodhiext,l(!0);return}if(Date.now()-n>=s){l(!1);return}setTimeout(a,o)};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 i={type:"extension",extension:"ready",extensionId:t,server:r.PENDING_EXTENSION_READY};if(e.testConnection)try{const s=await this.getServerState();this.setState({...i,server:s}),this.logger.info(`Server connectivity tested: ${s.status}`)}catch(s){this.logger.error("Failed to get server state:",s),this.setState({...i,server:r.BACKEND_SERVER_NOT_REACHABLE})}else this.setState(i);return this.state}async login(e){const t=await this.getAuthState();if(t.status==="authenticated")return t;this.ensureBodhiext();const i=e?.userRole??"scope_user_user",s=e?.flowType??"popup";e?.onProgress?.("requesting");const o=new r.AccessRequestBuilder(this.authClientId).requestedRole(i).flowType(s);if(e?.requested&&o.requested(e.requested),s==="redirect"){const u=e?.redirectUrl??this.config.redirectUri;o.redirectUrl(u)}const n=o.build(),c=await this.requestAccess(n),{id:l,review_url:a}=r.unwrapResponse(c);e?.onProgress?.("reviewing");let h;if(s==="popup"){const u=async()=>{const p=await this.getAccessRequestStatus(l);if(p.status>=400)return null;const{status:g,access_request_scope:E}=p.body;return g==="approved"?{approved:!0,accessRequestScope:E??void 0}:["denied","failed","expired"].includes(g)?{approved:!1,status:g}:null},d=await r.openPopupReview(a,u,{intervalMs:e?.pollIntervalMs??r.DEFAULT_POLL_INTERVAL_MS,timeoutMs:e?.pollTimeoutMs??r.DEFAULT_POLL_TIMEOUT_MS});d.approved||r.throwAccessRequestDenialError(d.status??"unknown"),h=d.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 i=localStorage.getItem(this.storageKeys.STATE);if(!i||i!==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 s=await this.getAuthState();if(s.status!=="authenticated")throw new Error("Login failed");return this.setAuthState(s),s}async exchangeCodeForTokens(e){const t=localStorage.getItem(this.storageKeys.CODE_VERIFIER);if(!t)throw new Error("Code verifier not found");const i=new URLSearchParams({grant_type:"authorization_code",client_id:this.authClientId,code:e,redirect_uri:this.config.redirectUri,code_verifier:t}),s=await fetch(this.authEndpoints.token,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:i});if(!s.ok){const n=await s.text();throw new Error(`Token exchange failed: ${s.status} ${n}`)}const o=await s.json();if(!o.access_token)throw new Error("No access token received");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 logout(){const e=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);if(e)try{const i=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:i})}catch(i){this.logger.warn("Token revocation failed:",i)}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,refreshToken:null,expiresAt:null,isTokenRefresh:!1};return this.setAuthState(t),t}async getAuthState(){const e=await this._getAccessTokenRaw();if(!e)return{status:"unauthenticated",user:null,accessToken:null,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1};try{return{status:"authenticated",user:r.extractUserInfo(e),accessToken:e,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1}}catch(t){return this.logger.error("Failed to parse token:",t),{status:"unauthenticated",user:null,accessToken:null,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1}}}async _getAccessTokenRaw(){const e=localStorage.getItem(this.storageKeys.ACCESS_TOKEN),t=localStorage.getItem(this.storageKeys.EXPIRES_AT);if(!e)return null;if(t){const i=parseInt(t,10);if(Date.now()>=i-5*1e3){const s=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);return s?this._tryRefreshToken(s):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 i=r.extractUserInfo(t.tokens.access_token);return this.setAuthState({status:"authenticated",user:i,accessToken:t.tokens.access_token,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!0}),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,refreshToken:null,expiresAt:null,isTokenRefresh:!1}),null}catch(t){this.logger.warn("Token refresh failed:",t)}throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"),r.createOperationError("auth_error","Access token expired and unable to refresh. Try logging out and logging in again.")}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(){try{const e=await this.sendApiRequest("GET","/bodhi/v1/info");if(e.status>=400)return r.BACKEND_SERVER_NOT_REACHABLE;const t=e.body,i=t.version||"unknown";switch(t.status){case"ready":return{status:"ready",version:i,error:null,deployment:t.deployment??null,client_id:t.client_id??null};case"setup":return r.backendServerNotReady("setup",i,void 0,t.deployment,t.client_id);case"resource_admin":return r.backendServerNotReady("resource_admin",i,void 0,t.deployment,t.client_id);case"error":return r.backendServerNotReady("error",i,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}}catch{return r.BACKEND_SERVER_NOT_REACHABLE}}async*stream(e,t,i,s,o=!0){this.ensureBodhiext();let n=s||{};if(o){const a=await this._getAccessTokenRaw();if(!a)throw r.createOperationError("auth_error","Not authenticated. Please log in first.");n={...n,Authorization:`Bearer ${a}`}}const l=this.bodhiext.sendStreamRequest(e,t,i,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 r.BodhiApiError||a instanceof r.BodhiError)throw a;if(a instanceof Error){const h=a;throw a.name==="BodhiApiError"&&typeof h.status=="number"&&h.body!=null?new r.BodhiApiError(h.status,h.body,a.message,h.headers):a.name==="BodhiError"&&typeof h.code=="string"?new r.BodhiError(h.code,a.message):new r.BodhiError("extension_error",a.message)}throw a}finally{l.releaseLock()}}async streamText(e,t,i,s,o=!0){this.ensureBodhiext();let n={...s};if(o){const a=await this._getAccessTokenRaw();a&&(n={...n,Authorization:`Bearer ${a}`})}const c=await this.bodhiext.sendStreamText(e,t,i,n);async function*l(a){const h=a.getReader();try{for(;;){const{done:u,value:d}=await h.read();if(u)break;yield d}}finally{h.releaseLock()}}return{status:c.status,headers:c.headers,body:l(c.body)}}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 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(i=>this.getAccessRequestStatus(i),e,t)}async performOAuthPkce(e){const t=r.generateCodeVerifier(),i=await r.generateCodeChallenge(t),s=r.generateCodeVerifier();localStorage.setItem(this.storageKeys.CODE_VERIFIER,t),localStorage.setItem(this.storageKeys.STATE,s);const o=e.split(" ").filter(Boolean),n=new URLSearchParams({response_type:"code",client_id:this.authClientId,redirect_uri:this.config.redirectUri,scope:o.join(" "),state:s,code_challenge:i,code_challenge_method:"S256"});return window.location.href=`${this.authEndpoints.authorize}?${n}`,new Promise(()=>{})}async handleAccessRequestCallback(e){const t=await this.getAccessRequestStatus(e),{status:i,access_request_scope:s}=r.unwrapResponse(t);localStorage.removeItem(this.storageKeys.ACCESS_REQUEST_ID),i!=="approved"&&r.throwAccessRequestDenialError(i);const o=`openid profile email roles ${s??""}`.trim();return this.performOAuthPkce(o)}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}}}function y(f){if(typeof window>"u")throw new Error("redirectUri required in non-browser environment");const e=f==="/"?"":f.replace(/\/$/,"");return`${window.location.origin}${e}/callback`}class A extends r.BaseFacadeClient{constructor(e,t,i){const s=t||{},o={basePath:s.basePath||"/",redirectUri:s.redirectUri||y(s.basePath||"/"),authServerUrl:s.authServerUrl||"https://id.getbodhi.app/realms/bodhi",logLevel:s.logLevel||"warn",apiTimeoutMs:s.apiTimeoutMs,storage:s.storage,initialTokens:s.initialTokens,initParams:s.initParams};super(e,o,i)}createLogger(e){return new r.Logger("WebUIClient",e.logLevel)}createStoragePrefix(e){return r.createStoragePrefixWithNamespace(e.basePath,r.STORAGE_PREFIXES.WEB)}createExtClient(e,t){return new R(this.authClientId,{authServerUrl:e.authServerUrl,redirectUri:e.redirectUri,basePath:e.basePath,logLevel:e.logLevel,apiTimeoutMs:e.apiTimeoutMs,initParams:e.initParams},t)}createDirectClient(e,t,i){return new w({authClientId:e,authServerUrl:t.authServerUrl,redirectUri:t.redirectUri,logLevel:t.logLevel,basePath:t.basePath,apiTimeoutMs:t.apiTimeoutMs,storage:t.storage,initialTokens:t.initialTokens},i)}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 k="production";Object.defineProperty(exports,"InMemoryStorage",{enumerable:!0,get:()=>r.InMemoryStorage});exports.DirectWebClient=w;exports.LocalStorageAdapter=S;exports.WEB_BUILD_MODE=k;exports.WebUIClient=A;
@@ -1,4 +1,4 @@
1
- import { DirectClientBase as O, createStoragePrefixWithBasePath as A, STORAGE_PREFIXES as k, AccessRequestBuilder as C, unwrapResponse as S, openPopupReview as b, DEFAULT_POLL_TIMEOUT_MS as x, DEFAULT_POLL_INTERVAL_MS as P, throwAccessRequestDenialError as E, generateCodeVerifier as _, generateCodeChallenge as U, EXTENSION_STATE_NOT_INITIALIZED as v, Logger as q, createOAuthEndpoints as K, NOOP_STATE_CALLBACK as L, createStorageKeys as M, createOperationError as y, BodhiError as u, BodhiApiError as w, EXTENSION_STATE_NOT_FOUND as D, PENDING_EXTENSION_READY as B, BACKEND_SERVER_NOT_REACHABLE as m, extractUserInfo as I, refreshAccessToken as N, backendServerNotReady as R, SERVER_ERROR_CODES as F, Chat as $, Models as z, Embeddings as V, Mcps as W, pollAccessRequestUntilResolved as H, BaseFacadeClient as X } from "@bodhiapp/bodhi-js-core";
1
+ import { DirectClientBase as O, createStoragePrefixWithNamespace as A, STORAGE_PREFIXES as k, AccessRequestBuilder as C, unwrapResponse as S, openPopupReview as b, DEFAULT_POLL_TIMEOUT_MS as x, DEFAULT_POLL_INTERVAL_MS as P, throwAccessRequestDenialError as E, generateCodeVerifier as _, generateCodeChallenge as U, EXTENSION_STATE_NOT_INITIALIZED as v, Logger as q, createOAuthEndpoints as K, NOOP_STATE_CALLBACK as L, createStorageKeys as M, createOperationError as y, BodhiError as u, BodhiApiError as w, EXTENSION_STATE_NOT_FOUND as D, PENDING_EXTENSION_READY as N, BACKEND_SERVER_NOT_REACHABLE as m, extractUserInfo as I, refreshAccessToken as B, backendServerNotReady as R, SERVER_ERROR_CODES as F, Chat as $, Models as z, Embeddings as V, Mcps as W, pollAccessRequestUntilResolved as H, BaseFacadeClient as X } from "@bodhiapp/bodhi-js-core";
2
2
  import { InMemoryStorage as ae } from "@bodhiapp/bodhi-js-core";
3
3
  class G {
4
4
  async get(e) {
@@ -227,7 +227,7 @@ class Z {
227
227
  type: "extension",
228
228
  extension: "ready",
229
229
  extensionId: t,
230
- server: B
230
+ server: N
231
231
  };
232
232
  if (e.testConnection)
233
233
  try {
@@ -435,7 +435,7 @@ class Z {
435
435
  async _doRefreshToken(e) {
436
436
  this.logger.debug("Refreshing access token");
437
437
  try {
438
- const t = await N(
438
+ const t = await B(
439
439
  this.authEndpoints.token,
440
440
  e,
441
441
  this.authClientId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bodhiapp/bodhi-js",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
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.37",
41
- "@bodhiapp/bodhi-js-core": "0.0.37",
42
- "@bodhiapp/ts-client": "0.1.31"
40
+ "@bodhiapp/bodhi-browser-types": "0.0.38",
41
+ "@bodhiapp/bodhi-js-core": "0.0.38",
42
+ "@bodhiapp/ts-client": "0.1.32"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@eslint/js": "^9.23.0",