@bodhiapp/bodhi-js 0.0.3 → 0.0.4
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-js-sdk/core/src/build-info.d.ts +1 -0
- package/dist/bodhi-js-sdk/core/src/facade-client-base.d.ts +1 -1
- package/dist/bodhi-js-sdk/core/src/index.d.ts +1 -0
- package/dist/bodhi-js-sdk/core/src/storage.d.ts +12 -0
- package/dist/bodhi-js-sdk/web/src/build-info.d.ts +1 -0
- package/dist/bodhi-js-sdk/web/src/constants.d.ts +15 -4
- package/dist/bodhi-js-sdk/web/src/direct-client.d.ts +1 -0
- package/dist/bodhi-js-sdk/web/src/ext-client.d.ts +2 -1
- package/dist/bodhi-js-sdk/web/src/facade-client.d.ts +2 -0
- package/dist/bodhi-js-sdk/web/src/index.d.ts +1 -0
- package/dist/bodhi-web.cjs.js +1 -1
- package/dist/bodhi-web.esm.js +135 -125
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const BUILD_MODE: string;
|
|
@@ -20,7 +20,7 @@ export declare abstract class BaseFacadeClient<TConfig, TExtClient extends IConn
|
|
|
20
20
|
protected authClientId: string;
|
|
21
21
|
protected config: TConfig;
|
|
22
22
|
protected onStateChange: StateChangeCallback;
|
|
23
|
-
constructor(authClientId: string, config: TConfig, onStateChange?: StateChangeCallback, storagePrefix?: string);
|
|
23
|
+
constructor(authClientId: string, config: TConfig, onStateChange?: StateChangeCallback, storagePrefix?: string, basePath?: string);
|
|
24
24
|
/**
|
|
25
25
|
* Create logger instance
|
|
26
26
|
* Subclasses extract logLevel from their specific config type
|
|
@@ -27,6 +27,18 @@ export declare const STORAGE_PREFIXES: {
|
|
|
27
27
|
readonly WEB: "bodhi:web";
|
|
28
28
|
readonly EXT: "bodhi:ext";
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Create storage prefix with basePath for path isolation
|
|
32
|
+
*
|
|
33
|
+
* @param basePath - Base path of app (e.g., '/', '/app1/')
|
|
34
|
+
* @param prefix - Storage prefix (e.g., 'bodhi:web', 'bodhijs:')
|
|
35
|
+
* @returns Combined prefix with basePath isolation
|
|
36
|
+
*
|
|
37
|
+
* Examples:
|
|
38
|
+
* - createStoragePrefixWithBasePath('/', 'bodhi:web') => '/:bodhi:web'
|
|
39
|
+
* - createStoragePrefixWithBasePath('/app1/', 'bodhi:web') => '/app1/:bodhi:web'
|
|
40
|
+
*/
|
|
41
|
+
export declare function createStoragePrefixWithBasePath(basePath: string, prefix: string): string;
|
|
30
42
|
/**
|
|
31
43
|
* User Preferences Storage Manager
|
|
32
44
|
*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const BUILD_MODE: string;
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*/
|
|
1
|
+
import { StorageKeys } from '../../core/src/index.ts';
|
|
2
|
+
|
|
4
3
|
export declare const POLL_INTERVAL = 500;
|
|
5
4
|
export declare const POLL_TIMEOUT = 5000;
|
|
6
5
|
/**
|
|
7
6
|
* LocalStorage keys for OAuth tokens and PKCE flow (namespaced with 'bodhi:web' prefix)
|
|
7
|
+
* @deprecated Use createWebStorageKeys(basePath) instead for basePath-aware keys
|
|
8
|
+
*/
|
|
9
|
+
export declare const STORAGE_KEYS: StorageKeys;
|
|
10
|
+
/**
|
|
11
|
+
* Create storage keys for web mode with optional basePath isolation
|
|
12
|
+
*
|
|
13
|
+
* @param basePath - Base path of app (e.g., '/', '/app1/'), defaults to '/'
|
|
14
|
+
* @returns Storage keys with basePath-aware prefix
|
|
15
|
+
*
|
|
16
|
+
* Examples:
|
|
17
|
+
* - createWebStorageKeys('/') => keys with 'bodhi:web:' prefix
|
|
18
|
+
* - createWebStorageKeys('/app1/') => keys with '/app1/:bodhi:web:' prefix
|
|
8
19
|
*/
|
|
9
|
-
export declare
|
|
20
|
+
export declare function createWebStorageKeys(basePath?: string): StorageKeys;
|
|
@@ -5,6 +5,7 @@ import { DirectClientBase, AuthLoggedIn, AuthLoggedOut, DirectClientBaseConfig,
|
|
|
5
5
|
*/
|
|
6
6
|
export interface DirectWebClientConfig extends DirectClientBaseConfig {
|
|
7
7
|
redirectUri: string;
|
|
8
|
+
basePath?: string;
|
|
8
9
|
}
|
|
9
10
|
/**
|
|
10
11
|
* DirectWebClient - Web mode implementation using browser redirect OAuth
|
|
@@ -23,7 +23,8 @@ export declare class WindowBodhiextClient implements IExtensionClient {
|
|
|
23
23
|
private authEndpoints;
|
|
24
24
|
private onStateChange;
|
|
25
25
|
private refreshPromise;
|
|
26
|
-
|
|
26
|
+
private storageKeys;
|
|
27
|
+
constructor(authClientId: string, config: WebClientConfig, onStateChange?: StateChangeCallback, basePath?: string);
|
|
27
28
|
/**
|
|
28
29
|
* Set client state and notify callback
|
|
29
30
|
*/
|
|
@@ -9,6 +9,7 @@ export interface WebClientConfig {
|
|
|
9
9
|
authServerUrl: string;
|
|
10
10
|
redirectUri: string;
|
|
11
11
|
userScope: UserScope;
|
|
12
|
+
basePath?: string;
|
|
12
13
|
logLevel: LogLevel;
|
|
13
14
|
initParams?: {
|
|
14
15
|
extension?: {
|
|
@@ -28,6 +29,7 @@ export declare class WebUIClient extends BaseFacadeClient<WebClientConfig, Windo
|
|
|
28
29
|
redirectUri: string;
|
|
29
30
|
authServerUrl?: string;
|
|
30
31
|
userScope?: UserScope;
|
|
32
|
+
basePath?: string;
|
|
31
33
|
logLevel?: LogLevel;
|
|
32
34
|
initParams?: {
|
|
33
35
|
extension?: {
|
package/dist/bodhi-web.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("@bodhiapp/bodhi-js-core");class S extends s.DirectClientBase{constructor(e,t){super({...e,storagePrefix:s.STORAGE_PREFIXES.DIRECT},"DirectWebClient",t),this.redirectUri=e.redirectUri}async login(){const e=await this.getAuthState();if(e.isLoggedIn)return e;const t=await this.requestResourceAccess(),r=`openid profile email roles ${this.userScope} ${t}`,o=s.generateCodeVerifier(),n=await s.generateCodeChallenge(o),i=s.generateCodeVerifier();localStorage.setItem(this.storageKeys.CODE_VERIFIER,o),localStorage.setItem(this.storageKeys.STATE,i);const c=new URL(this.authEndpoints.authorize);throw c.searchParams.set("client_id",this.authClientId),c.searchParams.set("response_type","code"),c.searchParams.set("redirect_uri",this.redirectUri),c.searchParams.set("scope",r),c.searchParams.set("code_challenge",n),c.searchParams.set("code_challenge_method","S256"),c.searchParams.set("state",i),window.location.href=c.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.isLoggedIn)throw new Error("Login failed");const n=o;return this.setAuthState(n),n}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={isLoggedIn:!1};return this.setAuthState(t),t}async requestResourceAccess(){const e=await this.sendApiRequest("POST","/bodhi/v1/apps/request-access",{app_client_id:this.authClientId},{},!1);if(s.isApiResultOperationError(e))throw new Error("Failed to get resource access scope from server");if(!s.isApiResultSuccess(e))throw new Error("Failed to get resource access scope from server: API error");const t=e.body.scope;return localStorage.setItem(this.storageKeys.RESOURCE_SCOPE,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 f=500,_=5e3,a=s.createStorageKeys(s.STORAGE_PREFIXES.WEB);class p{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}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 new Error("Client not initialized")}async sendExtRequest(e,t){return this.ensureBodhiext(),this.bodhiext.sendExtRequest(e,t)}async sendApiRequest(e,t,r,o,n){try{this.ensureBodhiext()}catch(i){return{error:{message:i instanceof Error?i.message:String(i),type:"extension_error"}}}try{let i=o||{};if(n){const l=await this._getAccessTokenRaw();if(!l)return{error:{message:"Not authenticated. Please log in first.",type:"extension_error"}};i={...i,Authorization:`Bearer ${l}`}}return await this.bodhiext.sendApiRequest(e,t,r,i)}catch(i){const c=i==null?void 0:i.error,l=(c==null?void 0:c.message)??(i instanceof Error?i.message:String(i)),h=(c==null?void 0:c.type)||"extension_error";return{error:{message:l,type:h}}}}getState(){return this.state}isClientInitialized(){return this.state.extension==="ready"}isServerReady(){return this.isClientInitialized()&&this.state.server.status==="ready"}async init(e={}){var o,n,i,c;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 l=e.timeoutMs??((n=(o=this.config.initParams)==null?void 0:o.extension)==null?void 0:n.timeoutMs)??_,h=e.intervalMs??((c=(i=this.config.initParams)==null?void 0:i.extension)==null?void 0:c.intervalMs)??f,d=Date.now();if(!await new Promise(u=>{const E=()=>{if(window.bodhiext){this.bodhiext=window.bodhiext,u(!0);return}if(Date.now()-d>=l){u(!1);return}setTimeout(E,h)};E()}))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 l=await this.getServerState();this.setState({...r,server:l}),this.logger.info(`Server connectivity tested: ${l.status}`)}catch(l){this.logger.error("Failed to get server state:",l),this.setState({...r,server:s.BACKEND_SERVER_NOT_REACHABLE})}else this.setState(r);return this.state}async requestResourceAccess(){this.ensureBodhiext();const e=await this.bodhiext.sendApiRequest("POST","/bodhi/v1/apps/request-access",{app_client_id:this.authClientId});if(!s.isApiResultSuccess(e))throw new Error("Failed to get resource access scope: API error");const t=e.body.scope;return localStorage.setItem(a.RESOURCE_SCOPE,t),t}async login(){const e=await this.getAuthState();if(e.isLoggedIn)return e;this.ensureBodhiext();const t=await this.requestResourceAccess(),r=s.generateCodeVerifier(),o=await s.generateCodeChallenge(r),n=s.generateCodeVerifier();localStorage.setItem(a.CODE_VERIFIER,r),localStorage.setItem(a.STATE,n);const i=["openid","profile","email","roles",this.config.userScope,t],c=new URLSearchParams({response_type:"code",client_id:this.authClientId,redirect_uri:this.config.redirectUri,scope:i.join(" "),state:n,code_challenge:o,code_challenge_method:"S256"}),l=`${this.authEndpoints.authorize}?${c}`;return window.location.href=l,new Promise(()=>{})}async handleOAuthCallback(e,t){const r=localStorage.getItem(a.STATE);if(!r||r!==t)throw new Error("Invalid state parameter - possible CSRF attack");await this.exchangeCodeForTokens(e),localStorage.removeItem(a.CODE_VERIFIER),localStorage.removeItem(a.STATE);const o=await this.getAuthState();if(!o.isLoggedIn)throw new Error("Login failed");return this.setAuthState(o),o}async exchangeCodeForTokens(e){const t=localStorage.getItem(a.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 i=await o.text();throw new Error(`Token exchange failed: ${o.status} ${i}`)}const n=await o.json();if(!n.access_token)throw new Error("No access token received");if(localStorage.setItem(a.ACCESS_TOKEN,n.access_token),n.refresh_token&&localStorage.setItem(a.REFRESH_TOKEN,n.refresh_token),n.expires_in){const i=Date.now()+n.expires_in*1e3;localStorage.setItem(a.EXPIRES_AT,i.toString())}}async logout(){const e=localStorage.getItem(a.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(a.ACCESS_TOKEN),localStorage.removeItem(a.REFRESH_TOKEN),localStorage.removeItem(a.EXPIRES_AT),localStorage.removeItem(a.CODE_VERIFIER),localStorage.removeItem(a.STATE),localStorage.removeItem(a.RESOURCE_SCOPE);const t={isLoggedIn:!1};return this.setAuthState(t),t}async getAuthState(){const e=await this._getAccessTokenRaw();if(!e)return{isLoggedIn:!1};try{return{isLoggedIn:!0,userInfo:s.extractUserInfo(e),accessToken:e}}catch(t){return this.logger.error("Failed to parse token:",t),{isLoggedIn:!1}}}async _getAccessTokenRaw(){const e=localStorage.getItem(a.ACCESS_TOKEN),t=localStorage.getItem(a.EXPIRES_AT);if(!e)return null;if(t){const r=parseInt(t,10);if(Date.now()>=r-5*1e3){const o=localStorage.getItem(a.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){this._storeRefreshedTokens(t);const r=s.extractUserInfo(t.access_token);return this.setAuthState({isLoggedIn:!0,userInfo:r,accessToken:t.access_token}),this.logger.info("Token refreshed successfully"),t.access_token}}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")}_storeRefreshedTokens(e){const t=Date.now()+e.expires_in*1e3;localStorage.setItem(a.ACCESS_TOKEN,e.access_token),localStorage.setItem(a.EXPIRES_AT,String(t)),e.refresh_token&&localStorage.setItem(a.REFRESH_TOKEN,e.refresh_token)}async pingApi(){return this.sendApiRequest("GET","/ping")}async fetchModels(){return this.sendApiRequest("GET","/v1/models",void 0,void 0,!0)}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"};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 i=o||{};if(n){const h=await this._getAccessTokenRaw();if(!h)throw new Error("Not authenticated. Please log in first.");i={...i,Authorization:`Bearer ${h}`}}const l=this.bodhiext.sendStreamRequest(e,t,r,i).getReader();try{for(;;){const{value:h,done:d}=await l.read();if(d||h!=null&&h.done)break;yield h.body}}catch(h){if(h instanceof Error){if("response"in h){const d=h;throw s.createApiError(h.message,d.response.status,d.response.body)}throw"error"in h,s.createOperationError(h.message,"extension_error")}throw h}finally{l.releaseLock()}}async*streamChat(e,t,r=!0){yield*this.stream("POST","/v1/chat/completions",{model:e,messages:[{role:"user",content:t}],stream:!0},void 0,r)}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}}}class m extends s.BaseFacadeClient{constructor(e,t,r,o){const n={redirectUri:t.redirectUri,authServerUrl:t.authServerUrl||"https://id.getbodhi.app/realms/bodhi",userScope:t.userScope||"scope_user_user",logLevel:t.logLevel||"warn",initParams:t.initParams};super(e,n,r,o)}createLogger(e){return new s.Logger("WebUIClient",e.logLevel)}createExtClient(e,t){return new p(this.authClientId,e,t)}createDirectClient(e,t,r){return new S({authClientId:e,authServerUrl:t.authServerUrl,redirectUri:t.redirectUri,userScope:t.userScope,logLevel:t.logLevel,storagePrefix:s.STORAGE_PREFIXES.WEB},r)}async handleOAuthCallback(e,t){return this.connectionMode==="direct"?this.directClient.handleOAuthCallback(e,t):this.extClient.handleOAuthCallback(e,t)}}exports.WebUIClient=m;
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("@bodhiapp/bodhi-js-core");class E extends s.DirectClientBase{constructor(e,t){const r=e.basePath||"/",o=s.createStoragePrefixWithBasePath(r,s.STORAGE_PREFIXES.DIRECT);super({...e,storagePrefix:o},"DirectWebClient",t),this.redirectUri=e.redirectUri}async login(){const e=await this.getAuthState();if(e.isLoggedIn)return e;const t=await this.requestResourceAccess(),r=`openid profile email roles ${this.userScope} ${t}`,o=s.generateCodeVerifier(),a=await s.generateCodeChallenge(o),i=s.generateCodeVerifier();localStorage.setItem(this.storageKeys.CODE_VERIFIER,o),localStorage.setItem(this.storageKeys.STATE,i);const n=new URL(this.authEndpoints.authorize);throw n.searchParams.set("client_id",this.authClientId),n.searchParams.set("response_type","code"),n.searchParams.set("redirect_uri",this.redirectUri),n.searchParams.set("scope",r),n.searchParams.set("code_challenge",a),n.searchParams.set("code_challenge_method","S256"),n.searchParams.set("state",i),window.location.href=n.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.isLoggedIn)throw new Error("Login failed");const a=o;return this.setAuthState(a),a}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={isLoggedIn:!1};return this.setAuthState(t),t}async requestResourceAccess(){const e=await this.sendApiRequest("POST","/bodhi/v1/apps/request-access",{app_client_id:this.authClientId},{},!1);if(s.isApiResultOperationError(e))throw new Error("Failed to get resource access scope from server");if(!s.isApiResultSuccess(e))throw new Error("Failed to get resource access scope from server: API error");const t=e.body.scope;return localStorage.setItem(this.storageKeys.RESOURCE_SCOPE,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 a=await r.text();throw new Error(`Token exchange failed: ${r.status} ${a}`)}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 a=Date.now()+o.expires_in*1e3;localStorage.setItem(this.storageKeys.EXPIRES_AT,a.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 S=500,f=5e3;s.createStorageKeys(s.STORAGE_PREFIXES.WEB);function _(d="/"){const e=s.createStoragePrefixWithBasePath(d,s.STORAGE_PREFIXES.WEB);return s.createStorageKeys(e)}class p{constructor(e,t,r,o){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,this.storageKeys=_(o||"/")}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 new Error("Client not initialized")}async sendExtRequest(e,t){return this.ensureBodhiext(),this.bodhiext.sendExtRequest(e,t)}async sendApiRequest(e,t,r,o,a){try{this.ensureBodhiext()}catch(i){return{error:{message:i instanceof Error?i.message:String(i),type:"extension_error"}}}try{let i=o||{};if(a){const h=await this._getAccessTokenRaw();if(!h)return{error:{message:"Not authenticated. Please log in first.",type:"extension_error"}};i={...i,Authorization:`Bearer ${h}`}}return await this.bodhiext.sendApiRequest(e,t,r,i)}catch(i){const n=i==null?void 0:i.error,h=(n==null?void 0:n.message)??(i instanceof Error?i.message:String(i)),c=(n==null?void 0:n.type)||"extension_error";return{error:{message:h,type:c}}}}getState(){return this.state}isClientInitialized(){return this.state.extension==="ready"}isServerReady(){return this.isClientInitialized()&&this.state.server.status==="ready"}async init(e={}){var o,a,i,n;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 h=e.timeoutMs??((a=(o=this.config.initParams)==null?void 0:o.extension)==null?void 0:a.timeoutMs)??f,c=e.intervalMs??((n=(i=this.config.initParams)==null?void 0:i.extension)==null?void 0:n.intervalMs)??S,l=Date.now();if(!await new Promise(g=>{const u=()=>{if(window.bodhiext){this.bodhiext=window.bodhiext,g(!0);return}if(Date.now()-l>=h){g(!1);return}setTimeout(u,c)};u()}))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 h=await this.getServerState();this.setState({...r,server:h}),this.logger.info(`Server connectivity tested: ${h.status}`)}catch(h){this.logger.error("Failed to get server state:",h),this.setState({...r,server:s.BACKEND_SERVER_NOT_REACHABLE})}else this.setState(r);return this.state}async requestResourceAccess(){this.ensureBodhiext();const e=await this.bodhiext.sendApiRequest("POST","/bodhi/v1/apps/request-access",{app_client_id:this.authClientId});if(!s.isApiResultSuccess(e))throw new Error("Failed to get resource access scope: API error");const t=e.body.scope;return localStorage.setItem(this.storageKeys.RESOURCE_SCOPE,t),t}async login(){const e=await this.getAuthState();if(e.isLoggedIn)return e;this.ensureBodhiext();const t=await this.requestResourceAccess(),r=s.generateCodeVerifier(),o=await s.generateCodeChallenge(r),a=s.generateCodeVerifier();localStorage.setItem(this.storageKeys.CODE_VERIFIER,r),localStorage.setItem(this.storageKeys.STATE,a);const i=["openid","profile","email","roles",this.config.userScope,t],n=new URLSearchParams({response_type:"code",client_id:this.authClientId,redirect_uri:this.config.redirectUri,scope:i.join(" "),state:a,code_challenge:o,code_challenge_method:"S256"}),h=`${this.authEndpoints.authorize}?${n}`;return window.location.href=h,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.isLoggedIn)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 i=await o.text();throw new Error(`Token exchange failed: ${o.status} ${i}`)}const a=await o.json();if(!a.access_token)throw new Error("No access token received");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){const i=Date.now()+a.expires_in*1e3;localStorage.setItem(this.storageKeys.EXPIRES_AT,i.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={isLoggedIn:!1};return this.setAuthState(t),t}async getAuthState(){const e=await this._getAccessTokenRaw();if(!e)return{isLoggedIn:!1};try{return{isLoggedIn:!0,userInfo:s.extractUserInfo(e),accessToken:e}}catch(t){return this.logger.error("Failed to parse token:",t),{isLoggedIn:!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 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){this._storeRefreshedTokens(t);const r=s.extractUserInfo(t.access_token);return this.setAuthState({isLoggedIn:!0,userInfo:r,accessToken:t.access_token}),this.logger.info("Token refreshed successfully"),t.access_token}}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")}_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 fetchModels(){return this.sendApiRequest("GET","/v1/models",void 0,void 0,!0)}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"};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,a=!0){this.ensureBodhiext();let i=o||{};if(a){const c=await this._getAccessTokenRaw();if(!c)throw new Error("Not authenticated. Please log in first.");i={...i,Authorization:`Bearer ${c}`}}const h=this.bodhiext.sendStreamRequest(e,t,r,i).getReader();try{for(;;){const{value:c,done:l}=await h.read();if(l||c!=null&&c.done)break;yield c.body}}catch(c){if(c instanceof Error){if("response"in c){const l=c;throw s.createApiError(c.message,l.response.status,l.response.body)}throw"error"in c,s.createOperationError(c.message,"extension_error")}throw c}finally{h.releaseLock()}}async*streamChat(e,t,r=!0){yield*this.stream("POST","/v1/chat/completions",{model:e,messages:[{role:"user",content:t}],stream:!0},void 0,r)}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}}}class y extends s.BaseFacadeClient{constructor(e,t,r,o){const a={redirectUri:t.redirectUri,authServerUrl:t.authServerUrl||"https://id.getbodhi.app/realms/bodhi",userScope:t.userScope||"scope_user_user",basePath:t.basePath||"/",logLevel:t.logLevel||"warn",initParams:t.initParams};super(e,a,r,o,t.basePath)}createLogger(e){return new s.Logger("WebUIClient",e.logLevel)}createExtClient(e,t){return new p(this.authClientId,e,t,e.basePath)}createDirectClient(e,t,r){return new E({authClientId:e,authServerUrl:t.authServerUrl,redirectUri:t.redirectUri,userScope:t.userScope,logLevel:t.logLevel,storagePrefix:s.STORAGE_PREFIXES.WEB,basePath:t.basePath},r)}async handleOAuthCallback(e,t){return this.connectionMode==="direct"?this.directClient.handleOAuthCallback(e,t):this.extClient.handleOAuthCallback(e,t)}}const m="production";exports.WEB_BUILD_MODE=m;exports.WebUIClient=y;
|
package/dist/bodhi-web.esm.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { DirectClientBase as
|
|
2
|
-
class L extends
|
|
1
|
+
import { DirectClientBase as x, createStoragePrefixWithBasePath as w, STORAGE_PREFIXES as u, generateCodeVerifier as g, generateCodeChallenge as I, isApiResultOperationError as T, isApiResultSuccess as f, createStorageKeys as R, EXTENSION_STATE_NOT_INITIALIZED as y, Logger as C, createOAuthEndpoints as A, NOOP_STATE_CALLBACK as k, EXTENSION_STATE_NOT_FOUND as v, PENDING_EXTENSION_READY as b, BACKEND_SERVER_NOT_REACHABLE as d, extractUserInfo as _, refreshAccessToken as P, createOperationError as S, backendServerNotReady as E, SERVER_ERROR_CODES as O, createApiError as K, BaseFacadeClient as U } from "@bodhiapp/bodhi-js-core";
|
|
2
|
+
class L extends x {
|
|
3
3
|
constructor(e, t) {
|
|
4
|
-
|
|
4
|
+
const s = e.basePath || "/", r = w(s, u.DIRECT);
|
|
5
|
+
super({ ...e, storagePrefix: r }, "DirectWebClient", t), this.redirectUri = e.redirectUri;
|
|
5
6
|
}
|
|
6
7
|
// ============================================================================
|
|
7
8
|
// Authentication (Browser Redirect OAuth)
|
|
@@ -10,27 +11,27 @@ class L extends C {
|
|
|
10
11
|
const e = await this.getAuthState();
|
|
11
12
|
if (e.isLoggedIn)
|
|
12
13
|
return e;
|
|
13
|
-
const t = await this.requestResourceAccess(),
|
|
14
|
-
localStorage.setItem(this.storageKeys.CODE_VERIFIER,
|
|
14
|
+
const t = await this.requestResourceAccess(), s = `openid profile email roles ${this.userScope} ${t}`, r = g(), i = await I(r), o = g();
|
|
15
|
+
localStorage.setItem(this.storageKeys.CODE_VERIFIER, r), localStorage.setItem(this.storageKeys.STATE, o);
|
|
15
16
|
const a = new URL(this.authEndpoints.authorize);
|
|
16
|
-
throw a.searchParams.set("client_id", this.authClientId), a.searchParams.set("response_type", "code"), a.searchParams.set("redirect_uri", this.redirectUri), a.searchParams.set("scope",
|
|
17
|
+
throw a.searchParams.set("client_id", this.authClientId), a.searchParams.set("response_type", "code"), a.searchParams.set("redirect_uri", this.redirectUri), a.searchParams.set("scope", s), a.searchParams.set("code_challenge", i), a.searchParams.set("code_challenge_method", "S256"), a.searchParams.set("state", o), window.location.href = a.toString(), new Error("Redirect initiated");
|
|
17
18
|
}
|
|
18
19
|
async handleOAuthCallback(e, t) {
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
20
|
+
const s = localStorage.getItem(this.storageKeys.STATE);
|
|
21
|
+
if (!s || s !== t)
|
|
21
22
|
throw new Error("Invalid state parameter - possible CSRF attack");
|
|
22
23
|
await this.exchangeCodeForTokens(e), localStorage.removeItem(this.storageKeys.CODE_VERIFIER), localStorage.removeItem(this.storageKeys.STATE);
|
|
23
|
-
const
|
|
24
|
-
if (!
|
|
24
|
+
const r = await this.getAuthState();
|
|
25
|
+
if (!r.isLoggedIn)
|
|
25
26
|
throw new Error("Login failed");
|
|
26
|
-
const i =
|
|
27
|
+
const i = r;
|
|
27
28
|
return this.setAuthState(i), i;
|
|
28
29
|
}
|
|
29
30
|
async logout() {
|
|
30
31
|
const e = localStorage.getItem(this.storageKeys.REFRESH_TOKEN);
|
|
31
32
|
if (e)
|
|
32
33
|
try {
|
|
33
|
-
const
|
|
34
|
+
const s = new URLSearchParams({
|
|
34
35
|
token: e,
|
|
35
36
|
client_id: this.authClientId,
|
|
36
37
|
token_type_hint: "refresh_token"
|
|
@@ -40,10 +41,10 @@ class L extends C {
|
|
|
40
41
|
headers: {
|
|
41
42
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
42
43
|
},
|
|
43
|
-
body:
|
|
44
|
+
body: s
|
|
44
45
|
});
|
|
45
|
-
} catch (
|
|
46
|
-
this.logger.warn("Token revocation failed:",
|
|
46
|
+
} catch (s) {
|
|
47
|
+
this.logger.warn("Token revocation failed:", s);
|
|
47
48
|
}
|
|
48
49
|
localStorage.removeItem(this.storageKeys.ACCESS_TOKEN), localStorage.removeItem(this.storageKeys.REFRESH_TOKEN), localStorage.removeItem(this.storageKeys.EXPIRES_AT), localStorage.removeItem(this.storageKeys.RESOURCE_SCOPE);
|
|
49
50
|
const t = {
|
|
@@ -64,7 +65,7 @@ class L extends C {
|
|
|
64
65
|
);
|
|
65
66
|
if (T(e))
|
|
66
67
|
throw new Error("Failed to get resource access scope from server");
|
|
67
|
-
if (!
|
|
68
|
+
if (!f(e))
|
|
68
69
|
throw new Error("Failed to get resource access scope from server: API error");
|
|
69
70
|
const t = e.body.scope;
|
|
70
71
|
return localStorage.setItem(this.storageKeys.RESOURCE_SCOPE, t), t;
|
|
@@ -73,7 +74,7 @@ class L extends C {
|
|
|
73
74
|
const t = localStorage.getItem(this.storageKeys.CODE_VERIFIER);
|
|
74
75
|
if (!t)
|
|
75
76
|
throw new Error("Code verifier not found");
|
|
76
|
-
const
|
|
77
|
+
const s = await fetch(this.authEndpoints.token, {
|
|
77
78
|
method: "POST",
|
|
78
79
|
headers: {
|
|
79
80
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
@@ -86,13 +87,13 @@ class L extends C {
|
|
|
86
87
|
code_verifier: t
|
|
87
88
|
})
|
|
88
89
|
});
|
|
89
|
-
if (!
|
|
90
|
-
const i = await
|
|
91
|
-
throw new Error(`Token exchange failed: ${
|
|
90
|
+
if (!s.ok) {
|
|
91
|
+
const i = await s.text();
|
|
92
|
+
throw new Error(`Token exchange failed: ${s.status} ${i}`);
|
|
92
93
|
}
|
|
93
|
-
const
|
|
94
|
-
if (localStorage.setItem(this.storageKeys.ACCESS_TOKEN,
|
|
95
|
-
const i = Date.now() +
|
|
94
|
+
const r = await s.json();
|
|
95
|
+
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) {
|
|
96
|
+
const i = Date.now() + r.expires_in * 1e3;
|
|
96
97
|
localStorage.setItem(this.storageKeys.EXPIRES_AT, i.toString());
|
|
97
98
|
}
|
|
98
99
|
}
|
|
@@ -103,8 +104,8 @@ class L extends C {
|
|
|
103
104
|
return localStorage.getItem(e);
|
|
104
105
|
}
|
|
105
106
|
async _storageSet(e) {
|
|
106
|
-
Object.entries(e).forEach(([t,
|
|
107
|
-
localStorage.setItem(t, String(
|
|
107
|
+
Object.entries(e).forEach(([t, s]) => {
|
|
108
|
+
localStorage.setItem(t, String(s));
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
111
|
async _storageRemove(e) {
|
|
@@ -114,10 +115,15 @@ class L extends C {
|
|
|
114
115
|
return this.redirectUri;
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
const N = 500, D = 5e3;
|
|
119
|
+
R(u.WEB);
|
|
120
|
+
function F(l = "/") {
|
|
121
|
+
const e = w(l, u.WEB);
|
|
122
|
+
return R(e);
|
|
123
|
+
}
|
|
124
|
+
class B {
|
|
125
|
+
constructor(e, t, s, r) {
|
|
126
|
+
this.state = y, this.bodhiext = null, this.refreshPromise = null, this.logger = new C("WindowBodhiextClient", t.logLevel), this.authClientId = e, this.config = t, this.authEndpoints = A(this.config.authServerUrl), this.onStateChange = s ?? k, this.storageKeys = F(r || "/");
|
|
121
127
|
}
|
|
122
128
|
/**
|
|
123
129
|
* Set client state and notify callback
|
|
@@ -158,7 +164,7 @@ class D {
|
|
|
158
164
|
* Send API message via window.bodhiext.sendApiRequest
|
|
159
165
|
* Converts ApiResponse to ApiResponseResult
|
|
160
166
|
*/
|
|
161
|
-
async sendApiRequest(e, t,
|
|
167
|
+
async sendApiRequest(e, t, s, r, i) {
|
|
162
168
|
try {
|
|
163
169
|
this.ensureBodhiext();
|
|
164
170
|
} catch (o) {
|
|
@@ -170,10 +176,10 @@ class D {
|
|
|
170
176
|
};
|
|
171
177
|
}
|
|
172
178
|
try {
|
|
173
|
-
let o =
|
|
179
|
+
let o = r || {};
|
|
174
180
|
if (i) {
|
|
175
|
-
const
|
|
176
|
-
if (!
|
|
181
|
+
const c = await this._getAccessTokenRaw();
|
|
182
|
+
if (!c)
|
|
177
183
|
return {
|
|
178
184
|
error: {
|
|
179
185
|
message: "Not authenticated. Please log in first.",
|
|
@@ -182,21 +188,21 @@ class D {
|
|
|
182
188
|
};
|
|
183
189
|
o = {
|
|
184
190
|
...o,
|
|
185
|
-
Authorization: `Bearer ${
|
|
191
|
+
Authorization: `Bearer ${c}`
|
|
186
192
|
};
|
|
187
193
|
}
|
|
188
194
|
return await this.bodhiext.sendApiRequest(
|
|
189
195
|
e,
|
|
190
196
|
t,
|
|
191
|
-
|
|
197
|
+
s,
|
|
192
198
|
o
|
|
193
199
|
);
|
|
194
200
|
} catch (o) {
|
|
195
|
-
const a = o == null ? void 0 : o.error,
|
|
201
|
+
const a = o == null ? void 0 : o.error, c = (a == null ? void 0 : a.message) ?? (o instanceof Error ? o.message : String(o)), n = (a == null ? void 0 : a.type) || "extension_error";
|
|
196
202
|
return {
|
|
197
203
|
error: {
|
|
198
|
-
message:
|
|
199
|
-
type:
|
|
204
|
+
message: c,
|
|
205
|
+
type: n
|
|
200
206
|
}
|
|
201
207
|
};
|
|
202
208
|
}
|
|
@@ -221,46 +227,46 @@ class D {
|
|
|
221
227
|
* No extensionId storage/restoration needed - window.bodhiext handle is ephemeral
|
|
222
228
|
*/
|
|
223
229
|
async init(e = {}) {
|
|
224
|
-
var
|
|
230
|
+
var r, i, o, a;
|
|
225
231
|
if (!e.testConnection && !e.selectedConnection)
|
|
226
|
-
return this.logger.info("No testConnection or selectedConnection, returning not-initialized state"),
|
|
232
|
+
return this.logger.info("No testConnection or selectedConnection, returning not-initialized state"), y;
|
|
227
233
|
if (this.bodhiext && !e.testConnection)
|
|
228
234
|
return this.logger.debug("Already have bodhiext handle, skipping polling"), this.state;
|
|
229
235
|
if (!this.bodhiext) {
|
|
230
|
-
const
|
|
236
|
+
const c = e.timeoutMs ?? ((i = (r = this.config.initParams) == null ? void 0 : r.extension) == null ? void 0 : i.timeoutMs) ?? D, n = e.intervalMs ?? ((a = (o = this.config.initParams) == null ? void 0 : o.extension) == null ? void 0 : a.intervalMs) ?? N, h = Date.now();
|
|
231
237
|
if (!await new Promise((p) => {
|
|
232
|
-
const
|
|
238
|
+
const m = () => {
|
|
233
239
|
if (window.bodhiext) {
|
|
234
240
|
this.bodhiext = window.bodhiext, p(!0);
|
|
235
241
|
return;
|
|
236
242
|
}
|
|
237
|
-
if (Date.now() -
|
|
243
|
+
if (Date.now() - h >= c) {
|
|
238
244
|
p(!1);
|
|
239
245
|
return;
|
|
240
246
|
}
|
|
241
|
-
setTimeout(
|
|
247
|
+
setTimeout(m, n);
|
|
242
248
|
};
|
|
243
|
-
|
|
249
|
+
m();
|
|
244
250
|
}))
|
|
245
251
|
return this.logger.warn("Extension discovery timed out"), this.setState(v), this.state;
|
|
246
252
|
}
|
|
247
253
|
const t = await this.bodhiext.getExtensionId();
|
|
248
254
|
this.logger.info(`Extension discovered: ${t}`);
|
|
249
|
-
const
|
|
255
|
+
const s = {
|
|
250
256
|
type: "extension",
|
|
251
257
|
extension: "ready",
|
|
252
258
|
extensionId: t,
|
|
253
|
-
server:
|
|
259
|
+
server: b
|
|
254
260
|
};
|
|
255
261
|
if (e.testConnection)
|
|
256
262
|
try {
|
|
257
|
-
const
|
|
258
|
-
this.setState({ ...
|
|
259
|
-
} catch (
|
|
260
|
-
this.logger.error("Failed to get server state:",
|
|
263
|
+
const c = await this.getServerState();
|
|
264
|
+
this.setState({ ...s, server: c }), this.logger.info(`Server connectivity tested: ${c.status}`);
|
|
265
|
+
} catch (c) {
|
|
266
|
+
this.logger.error("Failed to get server state:", c), this.setState({ ...s, server: d });
|
|
261
267
|
}
|
|
262
268
|
else
|
|
263
|
-
this.setState(
|
|
269
|
+
this.setState(s);
|
|
264
270
|
return this.state;
|
|
265
271
|
}
|
|
266
272
|
// ============================================================================
|
|
@@ -275,10 +281,10 @@ class D {
|
|
|
275
281
|
const e = await this.bodhiext.sendApiRequest("POST", "/bodhi/v1/apps/request-access", {
|
|
276
282
|
app_client_id: this.authClientId
|
|
277
283
|
});
|
|
278
|
-
if (!
|
|
284
|
+
if (!f(e))
|
|
279
285
|
throw new Error("Failed to get resource access scope: API error");
|
|
280
286
|
const t = e.body.scope;
|
|
281
|
-
return localStorage.setItem(
|
|
287
|
+
return localStorage.setItem(this.storageKeys.RESOURCE_SCOPE, t), t;
|
|
282
288
|
}
|
|
283
289
|
/**
|
|
284
290
|
* Login via browser redirect OAuth2 + PKCE flow
|
|
@@ -289,18 +295,18 @@ class D {
|
|
|
289
295
|
if (e.isLoggedIn)
|
|
290
296
|
return e;
|
|
291
297
|
this.ensureBodhiext();
|
|
292
|
-
const t = await this.requestResourceAccess(),
|
|
293
|
-
localStorage.setItem(
|
|
298
|
+
const t = await this.requestResourceAccess(), s = g(), r = await I(s), i = g();
|
|
299
|
+
localStorage.setItem(this.storageKeys.CODE_VERIFIER, s), localStorage.setItem(this.storageKeys.STATE, i);
|
|
294
300
|
const o = ["openid", "profile", "email", "roles", this.config.userScope, t], a = new URLSearchParams({
|
|
295
301
|
response_type: "code",
|
|
296
302
|
client_id: this.authClientId,
|
|
297
303
|
redirect_uri: this.config.redirectUri,
|
|
298
304
|
scope: o.join(" "),
|
|
299
305
|
state: i,
|
|
300
|
-
code_challenge:
|
|
306
|
+
code_challenge: r,
|
|
301
307
|
code_challenge_method: "S256"
|
|
302
|
-
}),
|
|
303
|
-
return window.location.href =
|
|
308
|
+
}), c = `${this.authEndpoints.authorize}?${a}`;
|
|
309
|
+
return window.location.href = c, new Promise(() => {
|
|
304
310
|
});
|
|
305
311
|
}
|
|
306
312
|
/**
|
|
@@ -309,45 +315,45 @@ class D {
|
|
|
309
315
|
* @returns AuthLoggedIn with login state and user info
|
|
310
316
|
*/
|
|
311
317
|
async handleOAuthCallback(e, t) {
|
|
312
|
-
const
|
|
313
|
-
if (!
|
|
318
|
+
const s = localStorage.getItem(this.storageKeys.STATE);
|
|
319
|
+
if (!s || s !== t)
|
|
314
320
|
throw new Error("Invalid state parameter - possible CSRF attack");
|
|
315
|
-
await this.exchangeCodeForTokens(e), localStorage.removeItem(
|
|
316
|
-
const
|
|
317
|
-
if (!
|
|
321
|
+
await this.exchangeCodeForTokens(e), localStorage.removeItem(this.storageKeys.CODE_VERIFIER), localStorage.removeItem(this.storageKeys.STATE);
|
|
322
|
+
const r = await this.getAuthState();
|
|
323
|
+
if (!r.isLoggedIn)
|
|
318
324
|
throw new Error("Login failed");
|
|
319
|
-
return this.setAuthState(
|
|
325
|
+
return this.setAuthState(r), r;
|
|
320
326
|
}
|
|
321
327
|
/**
|
|
322
328
|
* Exchange authorization code for tokens
|
|
323
329
|
*/
|
|
324
330
|
async exchangeCodeForTokens(e) {
|
|
325
|
-
const t = localStorage.getItem(
|
|
331
|
+
const t = localStorage.getItem(this.storageKeys.CODE_VERIFIER);
|
|
326
332
|
if (!t)
|
|
327
333
|
throw new Error("Code verifier not found");
|
|
328
|
-
const
|
|
334
|
+
const s = new URLSearchParams({
|
|
329
335
|
grant_type: "authorization_code",
|
|
330
336
|
client_id: this.authClientId,
|
|
331
337
|
code: e,
|
|
332
338
|
redirect_uri: this.config.redirectUri,
|
|
333
339
|
code_verifier: t
|
|
334
|
-
}),
|
|
340
|
+
}), r = await fetch(this.authEndpoints.token, {
|
|
335
341
|
method: "POST",
|
|
336
342
|
headers: {
|
|
337
343
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
338
344
|
},
|
|
339
|
-
body:
|
|
345
|
+
body: s
|
|
340
346
|
});
|
|
341
|
-
if (!
|
|
342
|
-
const o = await
|
|
343
|
-
throw new Error(`Token exchange failed: ${
|
|
347
|
+
if (!r.ok) {
|
|
348
|
+
const o = await r.text();
|
|
349
|
+
throw new Error(`Token exchange failed: ${r.status} ${o}`);
|
|
344
350
|
}
|
|
345
|
-
const i = await
|
|
351
|
+
const i = await r.json();
|
|
346
352
|
if (!i.access_token)
|
|
347
353
|
throw new Error("No access token received");
|
|
348
|
-
if (localStorage.setItem(
|
|
354
|
+
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) {
|
|
349
355
|
const o = Date.now() + i.expires_in * 1e3;
|
|
350
|
-
localStorage.setItem(
|
|
356
|
+
localStorage.setItem(this.storageKeys.EXPIRES_AT, o.toString());
|
|
351
357
|
}
|
|
352
358
|
}
|
|
353
359
|
/**
|
|
@@ -355,10 +361,10 @@ class D {
|
|
|
355
361
|
* @returns AuthLoggedOut with logged out state
|
|
356
362
|
*/
|
|
357
363
|
async logout() {
|
|
358
|
-
const e = localStorage.getItem(
|
|
364
|
+
const e = localStorage.getItem(this.storageKeys.REFRESH_TOKEN);
|
|
359
365
|
if (e)
|
|
360
366
|
try {
|
|
361
|
-
const
|
|
367
|
+
const s = new URLSearchParams({
|
|
362
368
|
token: e,
|
|
363
369
|
client_id: this.authClientId,
|
|
364
370
|
token_type_hint: "refresh_token"
|
|
@@ -368,12 +374,12 @@ class D {
|
|
|
368
374
|
headers: {
|
|
369
375
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
370
376
|
},
|
|
371
|
-
body:
|
|
377
|
+
body: s
|
|
372
378
|
});
|
|
373
|
-
} catch (
|
|
374
|
-
this.logger.warn("Token revocation failed:",
|
|
379
|
+
} catch (s) {
|
|
380
|
+
this.logger.warn("Token revocation failed:", s);
|
|
375
381
|
}
|
|
376
|
-
localStorage.removeItem(
|
|
382
|
+
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);
|
|
377
383
|
const t = {
|
|
378
384
|
isLoggedIn: !1
|
|
379
385
|
};
|
|
@@ -387,7 +393,7 @@ class D {
|
|
|
387
393
|
if (!e)
|
|
388
394
|
return { isLoggedIn: !1 };
|
|
389
395
|
try {
|
|
390
|
-
return { isLoggedIn: !0, userInfo:
|
|
396
|
+
return { isLoggedIn: !0, userInfo: _(e), accessToken: e };
|
|
391
397
|
} catch (t) {
|
|
392
398
|
return this.logger.error("Failed to parse token:", t), { isLoggedIn: !1 };
|
|
393
399
|
}
|
|
@@ -397,14 +403,14 @@ class D {
|
|
|
397
403
|
* Returns null if not logged in or token expired
|
|
398
404
|
*/
|
|
399
405
|
async _getAccessTokenRaw() {
|
|
400
|
-
const e = localStorage.getItem(
|
|
406
|
+
const e = localStorage.getItem(this.storageKeys.ACCESS_TOKEN), t = localStorage.getItem(this.storageKeys.EXPIRES_AT);
|
|
401
407
|
if (!e)
|
|
402
408
|
return null;
|
|
403
409
|
if (t) {
|
|
404
|
-
const
|
|
405
|
-
if (Date.now() >=
|
|
406
|
-
const
|
|
407
|
-
return
|
|
410
|
+
const s = parseInt(t, 10);
|
|
411
|
+
if (Date.now() >= s - 5 * 1e3) {
|
|
412
|
+
const r = localStorage.getItem(this.storageKeys.REFRESH_TOKEN);
|
|
413
|
+
return r ? this._tryRefreshToken(r) : null;
|
|
408
414
|
}
|
|
409
415
|
}
|
|
410
416
|
return e;
|
|
@@ -429,24 +435,24 @@ class D {
|
|
|
429
435
|
async _doRefreshToken(e) {
|
|
430
436
|
this.logger.debug("Refreshing access token");
|
|
431
437
|
try {
|
|
432
|
-
const t = await
|
|
438
|
+
const t = await P(
|
|
433
439
|
this.authEndpoints.token,
|
|
434
440
|
e,
|
|
435
441
|
this.authClientId
|
|
436
442
|
);
|
|
437
443
|
if (t) {
|
|
438
444
|
this._storeRefreshedTokens(t);
|
|
439
|
-
const
|
|
445
|
+
const s = _(t.access_token);
|
|
440
446
|
return this.setAuthState({
|
|
441
447
|
isLoggedIn: !0,
|
|
442
|
-
userInfo:
|
|
448
|
+
userInfo: s,
|
|
443
449
|
accessToken: t.access_token
|
|
444
450
|
}), this.logger.info("Token refreshed successfully"), t.access_token;
|
|
445
451
|
}
|
|
446
452
|
} catch (t) {
|
|
447
453
|
this.logger.warn("Token refresh failed:", t);
|
|
448
454
|
}
|
|
449
|
-
throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"),
|
|
455
|
+
throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"), S(
|
|
450
456
|
"Access token expired and unable to refresh. Try logging out and logging in again.",
|
|
451
457
|
"token_refresh_failed"
|
|
452
458
|
);
|
|
@@ -456,7 +462,7 @@ class D {
|
|
|
456
462
|
*/
|
|
457
463
|
_storeRefreshedTokens(e) {
|
|
458
464
|
const t = Date.now() + e.expires_in * 1e3;
|
|
459
|
-
localStorage.setItem(
|
|
465
|
+
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);
|
|
460
466
|
}
|
|
461
467
|
/**
|
|
462
468
|
* Ping API
|
|
@@ -484,21 +490,21 @@ class D {
|
|
|
484
490
|
const e = await this.sendApiRequest("GET", "/bodhi/v1/info");
|
|
485
491
|
if (T(e))
|
|
486
492
|
return d;
|
|
487
|
-
if (!
|
|
493
|
+
if (!f(e))
|
|
488
494
|
return d;
|
|
489
495
|
const t = e.body;
|
|
490
496
|
switch (t.status) {
|
|
491
497
|
case "ready":
|
|
492
498
|
return { status: "ready", version: t.version || "unknown" };
|
|
493
499
|
case "setup":
|
|
494
|
-
return
|
|
500
|
+
return E("setup", t.version || "unknown");
|
|
495
501
|
case "resource-admin":
|
|
496
|
-
return
|
|
502
|
+
return E("resource-admin", t.version || "unknown");
|
|
497
503
|
case "error":
|
|
498
|
-
return
|
|
504
|
+
return E(
|
|
499
505
|
"error",
|
|
500
506
|
t.version || "unknown",
|
|
501
|
-
t.error ? { message: t.error.message, type: t.error.type } :
|
|
507
|
+
t.error ? { message: t.error.message, type: t.error.type } : O.SERVER_NOT_READY
|
|
502
508
|
);
|
|
503
509
|
default:
|
|
504
510
|
return d;
|
|
@@ -508,43 +514,43 @@ class D {
|
|
|
508
514
|
* Generic streaming via window.bodhiext.sendStreamRequest
|
|
509
515
|
* Wraps ReadableStream as AsyncGenerator
|
|
510
516
|
*/
|
|
511
|
-
async *stream(e, t,
|
|
517
|
+
async *stream(e, t, s, r, i = !0) {
|
|
512
518
|
this.ensureBodhiext();
|
|
513
|
-
let o =
|
|
519
|
+
let o = r || {};
|
|
514
520
|
if (i) {
|
|
515
|
-
const
|
|
516
|
-
if (!
|
|
521
|
+
const n = await this._getAccessTokenRaw();
|
|
522
|
+
if (!n)
|
|
517
523
|
throw new Error("Not authenticated. Please log in first.");
|
|
518
524
|
o = {
|
|
519
525
|
...o,
|
|
520
|
-
Authorization: `Bearer ${
|
|
526
|
+
Authorization: `Bearer ${n}`
|
|
521
527
|
};
|
|
522
528
|
}
|
|
523
|
-
const
|
|
529
|
+
const c = this.bodhiext.sendStreamRequest(e, t, s, o).getReader();
|
|
524
530
|
try {
|
|
525
531
|
for (; ; ) {
|
|
526
|
-
const { value:
|
|
527
|
-
if (
|
|
532
|
+
const { value: n, done: h } = await c.read();
|
|
533
|
+
if (h || n != null && n.done)
|
|
528
534
|
break;
|
|
529
|
-
yield
|
|
535
|
+
yield n.body;
|
|
530
536
|
}
|
|
531
|
-
} catch (
|
|
532
|
-
if (
|
|
533
|
-
if ("response" in
|
|
534
|
-
const
|
|
535
|
-
throw
|
|
537
|
+
} catch (n) {
|
|
538
|
+
if (n instanceof Error) {
|
|
539
|
+
if ("response" in n) {
|
|
540
|
+
const h = n;
|
|
541
|
+
throw K(n.message, h.response.status, h.response.body);
|
|
536
542
|
}
|
|
537
|
-
throw "error" in
|
|
543
|
+
throw "error" in n ? S(n.message, "extension_error") : S(n.message, "extension_error");
|
|
538
544
|
}
|
|
539
|
-
throw
|
|
545
|
+
throw n;
|
|
540
546
|
} finally {
|
|
541
|
-
|
|
547
|
+
c.releaseLock();
|
|
542
548
|
}
|
|
543
549
|
}
|
|
544
550
|
/**
|
|
545
551
|
* Chat streaming
|
|
546
552
|
*/
|
|
547
|
-
async *streamChat(e, t,
|
|
553
|
+
async *streamChat(e, t, s = !0) {
|
|
548
554
|
yield* this.stream(
|
|
549
555
|
"POST",
|
|
550
556
|
"/v1/chat/completions",
|
|
@@ -554,7 +560,7 @@ class D {
|
|
|
554
560
|
stream: !0
|
|
555
561
|
},
|
|
556
562
|
void 0,
|
|
557
|
-
|
|
563
|
+
s
|
|
558
564
|
);
|
|
559
565
|
}
|
|
560
566
|
/**
|
|
@@ -581,24 +587,25 @@ class D {
|
|
|
581
587
|
};
|
|
582
588
|
}
|
|
583
589
|
}
|
|
584
|
-
class
|
|
585
|
-
constructor(e, t,
|
|
590
|
+
class W extends U {
|
|
591
|
+
constructor(e, t, s, r) {
|
|
586
592
|
const i = {
|
|
587
593
|
redirectUri: t.redirectUri,
|
|
588
594
|
authServerUrl: t.authServerUrl || "https://id.getbodhi.app/realms/bodhi",
|
|
589
595
|
userScope: t.userScope || "scope_user_user",
|
|
596
|
+
basePath: t.basePath || "/",
|
|
590
597
|
logLevel: t.logLevel || "warn",
|
|
591
598
|
initParams: t.initParams
|
|
592
599
|
};
|
|
593
|
-
super(e, i, r,
|
|
600
|
+
super(e, i, s, r, t.basePath);
|
|
594
601
|
}
|
|
595
602
|
createLogger(e) {
|
|
596
|
-
return new
|
|
603
|
+
return new C("WebUIClient", e.logLevel);
|
|
597
604
|
}
|
|
598
605
|
createExtClient(e, t) {
|
|
599
|
-
return new
|
|
606
|
+
return new B(this.authClientId, e, t, e.basePath);
|
|
600
607
|
}
|
|
601
|
-
createDirectClient(e, t,
|
|
608
|
+
createDirectClient(e, t, s) {
|
|
602
609
|
return new L(
|
|
603
610
|
{
|
|
604
611
|
authClientId: e,
|
|
@@ -606,9 +613,10 @@ class V extends N {
|
|
|
606
613
|
redirectUri: t.redirectUri,
|
|
607
614
|
userScope: t.userScope,
|
|
608
615
|
logLevel: t.logLevel,
|
|
609
|
-
storagePrefix:
|
|
616
|
+
storagePrefix: u.WEB,
|
|
617
|
+
basePath: t.basePath
|
|
610
618
|
},
|
|
611
|
-
|
|
619
|
+
s
|
|
612
620
|
);
|
|
613
621
|
}
|
|
614
622
|
// ============================================================================
|
|
@@ -618,6 +626,8 @@ class V extends N {
|
|
|
618
626
|
return this.connectionMode === "direct" ? this.directClient.handleOAuthCallback(e, t) : this.extClient.handleOAuthCallback(e, t);
|
|
619
627
|
}
|
|
620
628
|
}
|
|
629
|
+
const $ = "production";
|
|
621
630
|
export {
|
|
622
|
-
|
|
631
|
+
$ as WEB_BUILD_MODE,
|
|
632
|
+
W as WebUIClient
|
|
623
633
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bodhiapp/bodhi-js",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Web SDK for Bodhi Browser - window.bodhiext communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/bodhi-web.cjs.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"lint:fix": "prettier --write . && eslint . --ext .js,.jsx,.ts,.tsx --fix"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@bodhiapp/bodhi-js-core": "0.0.
|
|
37
|
+
"@bodhiapp/bodhi-js-core": "0.0.4",
|
|
38
38
|
"@bodhiapp/ts-client": "^0.1.7"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|