@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.
@@ -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
@@ -17,3 +17,4 @@ export * from './oauth';
17
17
  export * from './direct-client-base';
18
18
  export * from './facade-client-base';
19
19
  export { isOperationError, type OperationError } from '../../../bodhi-browser-ext/src/types';
20
+ export { BUILD_MODE as CORE_BUILD_MODE } from './build-info';
@@ -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
- * Constants for web2ext communication
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 const STORAGE_KEYS: import('../../core/src/index.ts').StorageKeys;
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
- constructor(authClientId: string, config: WebClientConfig, onStateChange?: StateChangeCallback);
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?: {
@@ -3,3 +3,4 @@ import { IWebUIClient } from './interface';
3
3
 
4
4
  export { WebUIClient } from './facade-client';
5
5
  export type { IWebUIClient, SerializedWebExtensionState };
6
+ export { BUILD_MODE as WEB_BUILD_MODE } from './build-info';
@@ -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;
@@ -1,7 +1,8 @@
1
- import { DirectClientBase as C, STORAGE_PREFIXES as f, generateCodeVerifier as g, generateCodeChallenge as y, isApiResultOperationError as T, isApiResultSuccess as E, createStorageKeys as x, EXTENSION_STATE_NOT_INITIALIZED as _, Logger as R, createOAuthEndpoints as A, NOOP_STATE_CALLBACK as k, EXTENSION_STATE_NOT_FOUND as v, PENDING_EXTENSION_READY as O, BACKEND_SERVER_NOT_REACHABLE as d, extractUserInfo as I, refreshAccessToken as b, createOperationError as u, backendServerNotReady as S, SERVER_ERROR_CODES as P, createApiError as U, BaseFacadeClient as N } from "@bodhiapp/bodhi-js-core";
2
- class L extends C {
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
- super({ ...e, storagePrefix: f.DIRECT }, "DirectWebClient", t), this.redirectUri = e.redirectUri;
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(), r = `openid profile email roles ${this.userScope} ${t}`, s = g(), i = await y(s), o = g();
14
- localStorage.setItem(this.storageKeys.CODE_VERIFIER, s), localStorage.setItem(this.storageKeys.STATE, o);
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", r), 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
+ 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 r = localStorage.getItem(this.storageKeys.STATE);
20
- if (!r || r !== t)
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 s = await this.getAuthState();
24
- if (!s.isLoggedIn)
24
+ const r = await this.getAuthState();
25
+ if (!r.isLoggedIn)
25
26
  throw new Error("Login failed");
26
- const i = s;
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 r = new URLSearchParams({
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: r
44
+ body: s
44
45
  });
45
- } catch (r) {
46
- this.logger.warn("Token revocation failed:", r);
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 (!E(e))
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 r = await fetch(this.authEndpoints.token, {
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 (!r.ok) {
90
- const i = await r.text();
91
- throw new Error(`Token exchange failed: ${r.status} ${i}`);
90
+ if (!s.ok) {
91
+ const i = await s.text();
92
+ throw new Error(`Token exchange failed: ${s.status} ${i}`);
92
93
  }
93
- const s = await r.json();
94
- if (localStorage.setItem(this.storageKeys.ACCESS_TOKEN, s.access_token), s.refresh_token && localStorage.setItem(this.storageKeys.REFRESH_TOKEN, s.refresh_token), s.expires_in) {
95
- const i = Date.now() + s.expires_in * 1e3;
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, r]) => {
107
- localStorage.setItem(t, String(r));
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 K = 500, F = 5e3, n = x(f.WEB);
118
- class D {
119
- constructor(e, t, r) {
120
- this.state = _, this.bodhiext = null, this.refreshPromise = null, this.logger = new R("WindowBodhiextClient", t.logLevel), this.authClientId = e, this.config = t, this.authEndpoints = A(this.config.authServerUrl), this.onStateChange = r ?? k;
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, r, s, i) {
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 = s || {};
179
+ let o = r || {};
174
180
  if (i) {
175
- const h = await this._getAccessTokenRaw();
176
- if (!h)
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 ${h}`
191
+ Authorization: `Bearer ${c}`
186
192
  };
187
193
  }
188
194
  return await this.bodhiext.sendApiRequest(
189
195
  e,
190
196
  t,
191
- r,
197
+ s,
192
198
  o
193
199
  );
194
200
  } catch (o) {
195
- const a = o == null ? void 0 : o.error, h = (a == null ? void 0 : a.message) ?? (o instanceof Error ? o.message : String(o)), c = (a == null ? void 0 : a.type) || "extension_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: h,
199
- type: c
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 s, i, o, a;
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 h = e.timeoutMs ?? ((i = (s = this.config.initParams) == null ? void 0 : s.extension) == null ? void 0 : i.timeoutMs) ?? F, c = e.intervalMs ?? ((a = (o = this.config.initParams) == null ? void 0 : o.extension) == null ? void 0 : a.intervalMs) ?? K, l = Date.now();
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 w = () => {
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() - l >= h) {
243
+ if (Date.now() - h >= c) {
238
244
  p(!1);
239
245
  return;
240
246
  }
241
- setTimeout(w, c);
247
+ setTimeout(m, n);
242
248
  };
243
- w();
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 r = {
255
+ const s = {
250
256
  type: "extension",
251
257
  extension: "ready",
252
258
  extensionId: t,
253
- server: O
259
+ server: b
254
260
  };
255
261
  if (e.testConnection)
256
262
  try {
257
- const h = await this.getServerState();
258
- this.setState({ ...r, server: h }), this.logger.info(`Server connectivity tested: ${h.status}`);
259
- } catch (h) {
260
- this.logger.error("Failed to get server state:", h), this.setState({ ...r, server: d });
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(r);
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 (!E(e))
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(n.RESOURCE_SCOPE, t), t;
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(), r = g(), s = await y(r), i = g();
293
- localStorage.setItem(n.CODE_VERIFIER, r), localStorage.setItem(n.STATE, i);
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: s,
306
+ code_challenge: r,
301
307
  code_challenge_method: "S256"
302
- }), h = `${this.authEndpoints.authorize}?${a}`;
303
- return window.location.href = h, new Promise(() => {
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 r = localStorage.getItem(n.STATE);
313
- if (!r || r !== t)
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(n.CODE_VERIFIER), localStorage.removeItem(n.STATE);
316
- const s = await this.getAuthState();
317
- if (!s.isLoggedIn)
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(s), s;
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(n.CODE_VERIFIER);
331
+ const t = localStorage.getItem(this.storageKeys.CODE_VERIFIER);
326
332
  if (!t)
327
333
  throw new Error("Code verifier not found");
328
- const r = new URLSearchParams({
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
- }), s = await fetch(this.authEndpoints.token, {
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: r
345
+ body: s
340
346
  });
341
- if (!s.ok) {
342
- const o = await s.text();
343
- throw new Error(`Token exchange failed: ${s.status} ${o}`);
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 s.json();
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(n.ACCESS_TOKEN, i.access_token), i.refresh_token && localStorage.setItem(n.REFRESH_TOKEN, i.refresh_token), i.expires_in) {
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(n.EXPIRES_AT, o.toString());
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(n.REFRESH_TOKEN);
364
+ const e = localStorage.getItem(this.storageKeys.REFRESH_TOKEN);
359
365
  if (e)
360
366
  try {
361
- const r = new URLSearchParams({
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: r
377
+ body: s
372
378
  });
373
- } catch (r) {
374
- this.logger.warn("Token revocation failed:", r);
379
+ } catch (s) {
380
+ this.logger.warn("Token revocation failed:", s);
375
381
  }
376
- localStorage.removeItem(n.ACCESS_TOKEN), localStorage.removeItem(n.REFRESH_TOKEN), localStorage.removeItem(n.EXPIRES_AT), localStorage.removeItem(n.CODE_VERIFIER), localStorage.removeItem(n.STATE), localStorage.removeItem(n.RESOURCE_SCOPE);
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: I(e), accessToken: e };
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(n.ACCESS_TOKEN), t = localStorage.getItem(n.EXPIRES_AT);
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 r = parseInt(t, 10);
405
- if (Date.now() >= r - 5 * 1e3) {
406
- const s = localStorage.getItem(n.REFRESH_TOKEN);
407
- return s ? this._tryRefreshToken(s) : null;
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 b(
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 r = I(t.access_token);
445
+ const s = _(t.access_token);
440
446
  return this.setAuthState({
441
447
  isLoggedIn: !0,
442
- userInfo: r,
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"), u(
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(n.ACCESS_TOKEN, e.access_token), localStorage.setItem(n.EXPIRES_AT, String(t)), e.refresh_token && localStorage.setItem(n.REFRESH_TOKEN, e.refresh_token);
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 (!E(e))
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 S("setup", t.version || "unknown");
500
+ return E("setup", t.version || "unknown");
495
501
  case "resource-admin":
496
- return S("resource-admin", t.version || "unknown");
502
+ return E("resource-admin", t.version || "unknown");
497
503
  case "error":
498
- return S(
504
+ return E(
499
505
  "error",
500
506
  t.version || "unknown",
501
- t.error ? { message: t.error.message, type: t.error.type } : P.SERVER_NOT_READY
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, r, s, i = !0) {
517
+ async *stream(e, t, s, r, i = !0) {
512
518
  this.ensureBodhiext();
513
- let o = s || {};
519
+ let o = r || {};
514
520
  if (i) {
515
- const c = await this._getAccessTokenRaw();
516
- if (!c)
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 ${c}`
526
+ Authorization: `Bearer ${n}`
521
527
  };
522
528
  }
523
- const h = this.bodhiext.sendStreamRequest(e, t, r, o).getReader();
529
+ const c = this.bodhiext.sendStreamRequest(e, t, s, o).getReader();
524
530
  try {
525
531
  for (; ; ) {
526
- const { value: c, done: l } = await h.read();
527
- if (l || c != null && c.done)
532
+ const { value: n, done: h } = await c.read();
533
+ if (h || n != null && n.done)
528
534
  break;
529
- yield c.body;
535
+ yield n.body;
530
536
  }
531
- } catch (c) {
532
- if (c instanceof Error) {
533
- if ("response" in c) {
534
- const l = c;
535
- throw U(c.message, l.response.status, l.response.body);
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 c ? u(c.message, "extension_error") : u(c.message, "extension_error");
543
+ throw "error" in n ? S(n.message, "extension_error") : S(n.message, "extension_error");
538
544
  }
539
- throw c;
545
+ throw n;
540
546
  } finally {
541
- h.releaseLock();
547
+ c.releaseLock();
542
548
  }
543
549
  }
544
550
  /**
545
551
  * Chat streaming
546
552
  */
547
- async *streamChat(e, t, r = !0) {
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
- r
563
+ s
558
564
  );
559
565
  }
560
566
  /**
@@ -581,24 +587,25 @@ class D {
581
587
  };
582
588
  }
583
589
  }
584
- class V extends N {
585
- constructor(e, t, r, s) {
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, s);
600
+ super(e, i, s, r, t.basePath);
594
601
  }
595
602
  createLogger(e) {
596
- return new R("WebUIClient", e.logLevel);
603
+ return new C("WebUIClient", e.logLevel);
597
604
  }
598
605
  createExtClient(e, t) {
599
- return new D(this.authClientId, e, t);
606
+ return new B(this.authClientId, e, t, e.basePath);
600
607
  }
601
- createDirectClient(e, t, r) {
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: f.WEB
616
+ storagePrefix: u.WEB,
617
+ basePath: t.basePath
610
618
  },
611
- r
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
- V as WebUIClient
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",
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.3",
37
+ "@bodhiapp/bodhi-js-core": "0.0.4",
38
38
  "@bodhiapp/ts-client": "^0.1.7"
39
39
  },
40
40
  "devDependencies": {