@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.
- package/dist/bodhi-web.cjs.js +1 -1
- package/dist/bodhi-web.esm.js +288 -267
- package/dist/direct-client.d.ts +3 -6
- package/dist/ext-client.d.ts +7 -1
- package/dist/facade-client.d.ts +6 -2
- package/dist/index.d.ts +4 -0
- package/dist/local-storage.d.ts +6 -0
- package/package.json +4 -4
package/dist/bodhi-web.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const 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;
|
package/dist/bodhi-web.esm.js
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
|
-
import { DirectClientBase as
|
|
2
|
-
|
|
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
|
|
18
|
+
const r = A(
|
|
5
19
|
e.basePath,
|
|
6
|
-
|
|
7
|
-
),
|
|
20
|
+
k.WEB_DIRECT
|
|
21
|
+
), s = {
|
|
8
22
|
authClientId: e.authClientId,
|
|
9
23
|
authServerUrl: e.authServerUrl,
|
|
10
|
-
storagePrefix:
|
|
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(
|
|
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
|
|
40
|
+
const r = e?.userRole ?? "scope_user_user", s = e?.flowType ?? "popup";
|
|
25
41
|
e?.onProgress?.("requesting");
|
|
26
|
-
const
|
|
27
|
-
if (e?.requested &&
|
|
28
|
-
const
|
|
29
|
-
|
|
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 =
|
|
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 (
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
const { status:
|
|
39
|
-
return
|
|
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
|
-
|
|
60
|
+
h.approved || E(h.status ?? "unknown"), l = h.accessRequestScope;
|
|
45
61
|
} else
|
|
46
|
-
return
|
|
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 = _(),
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
58
|
-
if (!
|
|
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)
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
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(
|
|
83
|
+
return this.setAuthState(s), s;
|
|
65
84
|
}
|
|
66
85
|
async handleAccessRequestCallback(e) {
|
|
67
|
-
const t = await this.getAccessRequestStatus(e), { status:
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
return this.performOAuthPkce(
|
|
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
|
|
149
|
-
class
|
|
150
|
-
constructor(e, t,
|
|
151
|
-
this.state =
|
|
152
|
-
const
|
|
153
|
-
this.storageKeys =
|
|
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
|
|
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,
|
|
141
|
+
async sendApiRequest(e, t, r, s, i) {
|
|
195
142
|
this.ensureBodhiext();
|
|
196
143
|
try {
|
|
197
144
|
const a = new Promise(
|
|
198
|
-
(
|
|
199
|
-
() =>
|
|
200
|
-
new
|
|
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
|
-
),
|
|
208
|
-
let
|
|
209
|
-
if (
|
|
210
|
-
const
|
|
211
|
-
if (!
|
|
212
|
-
throw new
|
|
213
|
-
|
|
214
|
-
...
|
|
215
|
-
Authorization: `Bearer ${
|
|
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,
|
|
165
|
+
return this.bodhiext.sendApiRequest(e, t, r, c);
|
|
219
166
|
})();
|
|
220
|
-
return await Promise.race([
|
|
167
|
+
return await Promise.race([n, a]);
|
|
221
168
|
} catch (a) {
|
|
222
|
-
if (a instanceof
|
|
169
|
+
if (a instanceof w || a instanceof u) throw a;
|
|
223
170
|
if (a instanceof Error) {
|
|
224
|
-
const
|
|
225
|
-
throw a.name === "BodhiApiError" && typeof
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
) : a.name === "BodhiError" && typeof
|
|
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
|
|
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"),
|
|
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
|
|
261
|
-
if (!await new Promise((
|
|
262
|
-
const
|
|
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,
|
|
211
|
+
this.bodhiext = window.bodhiext, c(!0);
|
|
265
212
|
return;
|
|
266
213
|
}
|
|
267
|
-
if (Date.now() - a >=
|
|
268
|
-
|
|
214
|
+
if (Date.now() - a >= s) {
|
|
215
|
+
c(!1);
|
|
269
216
|
return;
|
|
270
217
|
}
|
|
271
|
-
setTimeout(
|
|
218
|
+
setTimeout(o, i);
|
|
272
219
|
};
|
|
273
|
-
|
|
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
|
|
226
|
+
const r = {
|
|
280
227
|
type: "extension",
|
|
281
228
|
extension: "ready",
|
|
282
229
|
extensionId: t,
|
|
283
|
-
server:
|
|
230
|
+
server: B
|
|
284
231
|
};
|
|
285
232
|
if (e.testConnection)
|
|
286
233
|
try {
|
|
287
|
-
const
|
|
288
|
-
this.setState({ ...
|
|
289
|
-
} catch (
|
|
290
|
-
this.logger.error("Failed to get server state:",
|
|
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(
|
|
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
|
|
256
|
+
const r = e?.userRole ?? "scope_user_user", s = e?.flowType ?? "popup";
|
|
310
257
|
e?.onProgress?.("requesting");
|
|
311
|
-
const
|
|
312
|
-
if (e?.requested &&
|
|
313
|
-
const
|
|
314
|
-
|
|
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 =
|
|
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 (
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
if (
|
|
323
|
-
const { status:
|
|
324
|
-
return
|
|
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
|
-
|
|
276
|
+
h.approved || E(h.status ?? "unknown"), l = h.accessRequestScope;
|
|
330
277
|
} else
|
|
331
|
-
return localStorage.setItem(this.storageKeys.ACCESS_REQUEST_ID,
|
|
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
|
|
342
|
-
if (!
|
|
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
|
|
346
|
-
if (
|
|
292
|
+
const s = await this.getAuthState();
|
|
293
|
+
if (s.status !== "authenticated")
|
|
347
294
|
throw new Error("Login failed");
|
|
348
|
-
return this.setAuthState(
|
|
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
|
|
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
|
-
}),
|
|
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:
|
|
315
|
+
body: r
|
|
369
316
|
});
|
|
370
|
-
if (!
|
|
371
|
-
const a = await
|
|
372
|
-
throw new Error(`Token exchange failed: ${
|
|
317
|
+
if (!s.ok) {
|
|
318
|
+
const a = await s.text();
|
|
319
|
+
throw new Error(`Token exchange failed: ${s.status} ${a}`);
|
|
373
320
|
}
|
|
374
|
-
const
|
|
375
|
-
if (!
|
|
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,
|
|
378
|
-
const a = Date.now() +
|
|
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
|
|
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:
|
|
347
|
+
body: r
|
|
401
348
|
});
|
|
402
|
-
} catch (
|
|
403
|
-
this.logger.warn("Token revocation failed:",
|
|
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 {
|
|
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 {
|
|
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), {
|
|
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
|
|
437
|
-
if (Date.now() >=
|
|
438
|
-
const
|
|
439
|
-
return
|
|
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
|
|
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
|
|
445
|
+
const r = I(t.tokens.access_token);
|
|
472
446
|
return this.setAuthState({
|
|
473
447
|
status: "authenticated",
|
|
474
|
-
user:
|
|
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"),
|
|
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
|
|
519
|
-
const t = e.body,
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
549
|
-
t.error ? { message: t.error.message, type: t.error.type } :
|
|
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
|
|
534
|
+
return m;
|
|
555
535
|
}
|
|
556
536
|
} catch {
|
|
557
|
-
return
|
|
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,
|
|
544
|
+
async *stream(e, t, r, s, i = !0) {
|
|
565
545
|
this.ensureBodhiext();
|
|
566
|
-
let a =
|
|
567
|
-
if (
|
|
568
|
-
const
|
|
569
|
-
if (!
|
|
570
|
-
throw
|
|
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 ${
|
|
553
|
+
Authorization: `Bearer ${o}`
|
|
574
554
|
};
|
|
575
555
|
}
|
|
576
|
-
const
|
|
556
|
+
const c = this.bodhiext.sendStreamRequest(e, t, r, a).getReader();
|
|
577
557
|
try {
|
|
578
558
|
for (; ; ) {
|
|
579
|
-
const { value:
|
|
580
|
-
if (l ||
|
|
559
|
+
const { value: o, done: l } = await c.read();
|
|
560
|
+
if (l || o?.done)
|
|
581
561
|
break;
|
|
582
|
-
yield
|
|
562
|
+
yield o.body;
|
|
583
563
|
}
|
|
584
|
-
} catch (
|
|
585
|
-
if (
|
|
586
|
-
if (
|
|
587
|
-
const l =
|
|
588
|
-
throw
|
|
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
|
-
|
|
571
|
+
o.message,
|
|
592
572
|
l.headers
|
|
593
|
-
) :
|
|
573
|
+
) : o.name === "BodhiError" && typeof l.code == "string" ? new u(l.code, o.message) : new u("extension_error", o.message);
|
|
594
574
|
}
|
|
595
|
-
throw
|
|
575
|
+
throw o;
|
|
596
576
|
} finally {
|
|
597
|
-
|
|
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
|
|
621
|
+
return this._models ??= new z(this);
|
|
608
622
|
}
|
|
609
623
|
get embeddings() {
|
|
610
|
-
return this._embeddings ??= new
|
|
624
|
+
return this._embeddings ??= new V(this);
|
|
611
625
|
}
|
|
612
626
|
get mcps() {
|
|
613
|
-
return this._mcps ??= new
|
|
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
|
|
638
|
-
(
|
|
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 = _(),
|
|
645
|
-
localStorage.setItem(this.storageKeys.CODE_VERIFIER, t), localStorage.setItem(this.storageKeys.STATE,
|
|
646
|
-
const
|
|
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:
|
|
651
|
-
state:
|
|
652
|
-
code_challenge:
|
|
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:
|
|
660
|
-
localStorage.removeItem(this.storageKeys.ACCESS_REQUEST_ID),
|
|
661
|
-
const
|
|
662
|
-
return this.performOAuthPkce(
|
|
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
|
|
701
|
+
function ee(f) {
|
|
688
702
|
if (typeof window > "u")
|
|
689
703
|
throw new Error("redirectUri required in non-browser environment");
|
|
690
|
-
const e =
|
|
704
|
+
const e = f === "/" ? "" : f.replace(/\/$/, "");
|
|
691
705
|
return `${window.location.origin}${e}/callback`;
|
|
692
706
|
}
|
|
693
|
-
class
|
|
694
|
-
constructor(e, t,
|
|
695
|
-
const
|
|
696
|
-
basePath:
|
|
697
|
-
redirectUri:
|
|
698
|
-
authServerUrl:
|
|
699
|
-
logLevel:
|
|
700
|
-
apiTimeoutMs:
|
|
701
|
-
|
|
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,
|
|
719
|
+
super(e, i, r);
|
|
704
720
|
}
|
|
705
721
|
createLogger(e) {
|
|
706
|
-
return new
|
|
722
|
+
return new q("WebUIClient", e.logLevel);
|
|
707
723
|
}
|
|
708
724
|
createStoragePrefix(e) {
|
|
709
|
-
return
|
|
725
|
+
return A(e.basePath, k.WEB);
|
|
710
726
|
}
|
|
711
727
|
createExtClient(e, t) {
|
|
712
|
-
return new
|
|
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,
|
|
726
|
-
return new
|
|
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
|
-
|
|
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
|
|
766
|
+
const re = "production";
|
|
749
767
|
export {
|
|
750
|
-
|
|
751
|
-
|
|
768
|
+
j as DirectWebClient,
|
|
769
|
+
ae as InMemoryStorage,
|
|
770
|
+
G as LocalStorageAdapter,
|
|
771
|
+
re as WEB_BUILD_MODE,
|
|
772
|
+
se as WebUIClient
|
|
752
773
|
};
|
package/dist/direct-client.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/ext-client.d.ts
CHANGED
|
@@ -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;
|
package/dist/facade-client.d.ts
CHANGED
|
@@ -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';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bodhiapp/bodhi-js",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
41
|
-
"@bodhiapp/bodhi-js-core": "0.0.
|
|
42
|
-
"@bodhiapp/ts-client": "0.1.
|
|
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",
|