@bodhiapp/bodhi-js 0.0.31 → 0.0.33

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