@bodhiapp/bodhi-js 0.0.23 → 0.0.24

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