@bodhiapp/bodhi-js 0.0.4 → 0.0.5
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-browser-ext/src/types/bodhiext.d.ts +0 -1
- package/dist/bodhi-browser-ext/src/types/protocol.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/errors.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/facade-client-base.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/interface.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/logger.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/oauth.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/onboarding/config.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/onboarding/modal.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/onboarding/protocol-utils.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/platform.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/types/api.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/types/callback.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/types/client-state.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/types/config.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/types/platform.d.ts +0 -1
- package/dist/bodhi-js-sdk/web/src/constants.d.ts +0 -1
- package/dist/bodhi-js-sdk/web/src/direct-client.d.ts +0 -1
- package/dist/bodhi-js-sdk/web/src/ext-client.d.ts +1 -2
- package/dist/bodhi-js-sdk/web/src/facade-client.d.ts +0 -1
- package/dist/bodhi-js-sdk/web/src/index.d.ts +0 -1
- package/dist/bodhi-web.cjs.js +1 -1
- package/dist/bodhi-web.esm.d.ts +1 -0
- package/dist/bodhi-web.esm.js +125 -126
- package/dist/setup-modal/src/types/message-types.d.ts +0 -1
- package/dist/setup-modal/src/types/protocol.d.ts +0 -1
- package/dist/setup-modal/src/types/state.d.ts +0 -1
- package/dist/setup-modal/src/types/type-guards.d.ts +0 -1
- package/package.json +6 -5
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { OpenAiApiError, PingResponse, CreateChatCompletionRequest, CreateChatCompletionResponse, CreateChatCompletionStreamResponse } from '@bodhiapp/ts-client';
|
|
2
|
-
|
|
3
2
|
/**
|
|
4
3
|
* HTTP response wrapper - body can be success type OR error type
|
|
5
4
|
* Use isApiErrorResponse() to narrow the type based on status
|
|
@@ -3,7 +3,6 @@ import { IConnectionClient } from './interface';
|
|
|
3
3
|
import { Logger } from './logger';
|
|
4
4
|
import { BodhiClientUserPrefsManager } from './storage';
|
|
5
5
|
import { ApiResponseResult, AuthLoggedIn, AuthLoggedOut, AuthState, BackendServerState, ClientState, ConnectionMode, DirectState, ExtensionState, InitParams, SerializedClientState, SerializedDirectState, SerializedExtensionState, StateChange, StateChangeCallback } from './types';
|
|
6
|
-
|
|
7
6
|
/**
|
|
8
7
|
* Base facade client with common delegation logic
|
|
9
8
|
*
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { CreateChatCompletionStreamResponse } from '@bodhiapp/ts-client';
|
|
2
2
|
import { ApiResponseResult, AuthLoggedIn, AuthLoggedOut, AuthState, BackendServerState, ClientState, ConnectionMode, DirectState, ExtensionState, InitParams, StateChangeCallback } from './types';
|
|
3
|
-
|
|
4
3
|
/**
|
|
5
4
|
* ConnectionClient - Base interface for all client implementations
|
|
6
5
|
*
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { MessageType, RequestPayload, ResponsePayload, RequestMessage, RequestId } from '../../../../setup-modal/src/types';
|
|
2
|
-
|
|
3
2
|
/** Build fire-and-forget event message */
|
|
4
3
|
export declare function buildEvent<T extends MessageType>(type: T, payload: RequestPayload<T>): {
|
|
5
4
|
kind: "event";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ApiResponseResult, AuthLoggedIn, AuthLoggedOut, AuthState, BackendServerState, ClientState, ExtensionState, IExtensionClient, InitParams, StateChangeCallback } from '../../core/src/index.ts';
|
|
2
2
|
import { CreateChatCompletionStreamResponse } from '@bodhiapp/ts-client';
|
|
3
3
|
import { WebClientConfig } from './facade-client';
|
|
4
|
-
|
|
5
4
|
export type SerializedWebExtensionState = {
|
|
6
5
|
extensionId?: string;
|
|
7
6
|
};
|
|
@@ -39,7 +38,7 @@ export declare class WindowBodhiextClient implements IExtensionClient {
|
|
|
39
38
|
setStateCallback(callback: StateChangeCallback): void;
|
|
40
39
|
/**
|
|
41
40
|
* Ensure bodhiext is available, attempting to acquire it if not already set
|
|
42
|
-
* @throws
|
|
41
|
+
* @throws OperationError if client not initialized
|
|
43
42
|
*/
|
|
44
43
|
private ensureBodhiext;
|
|
45
44
|
/**
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { BaseFacadeClient, Logger, AuthLoggedIn, IWebUIClient, LogLevel, StateChange, StateChangeCallback, UserScope } from '../../core/src/index.ts';
|
|
2
2
|
import { DirectWebClient } from './direct-client';
|
|
3
3
|
import { WindowBodhiextClient } from './ext-client';
|
|
4
|
-
|
|
5
4
|
/**
|
|
6
5
|
* Configuration for WebClient OAuth
|
|
7
6
|
*/
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { SerializedWebExtensionState } from './ext-client';
|
|
2
2
|
import { IWebUIClient } from './interface';
|
|
3
|
-
|
|
4
3
|
export { WebUIClient } from './facade-client';
|
|
5
4
|
export type { IWebUIClient, SerializedWebExtensionState };
|
|
6
5
|
export { BUILD_MODE as WEB_BUILD_MODE } from './build-info';
|
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 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
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("@bodhiapp/bodhi-js-core");class g 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 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",a),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 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 u=500,E=5e3;s.createStorageKeys(s.STORAGE_PREFIXES.WEB);function S(l="/"){const e=s.createStoragePrefixWithBasePath(l,s.STORAGE_PREFIXES.WEB);return s.createStorageKeys(e)}class f{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=S(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 s.createOperationError("Client not initialized","extension_error")}async sendExtRequest(e,t){return this.ensureBodhiext(),this.bodhiext.sendExtRequest(e,t)}async sendApiRequest(e,t,r,o,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 c=i?.error,h=c?.message??(i instanceof Error?i.message:String(i)),n=c?.type||"extension_error";return{error:{message:h,type:n}}}}getState(){return this.state}isClientInitialized(){return this.state.extension==="ready"}isServerReady(){return this.isClientInitialized()&&this.state.server.status==="ready"}async init(e={}){if(!e.testConnection&&!e.selectedConnection)return this.logger.info("No testConnection or selectedConnection, returning not-initialized state"),s.EXTENSION_STATE_NOT_INITIALIZED;if(this.bodhiext&&!e.testConnection)return this.logger.debug("Already have bodhiext handle, skipping polling"),this.state;if(!this.bodhiext){const o=e.timeoutMs??this.config.initParams?.extension?.timeoutMs??E,a=e.intervalMs??this.config.initParams?.extension?.intervalMs??u,i=Date.now();if(!await new Promise(h=>{const n=()=>{if(window.bodhiext){this.bodhiext=window.bodhiext,h(!0);return}if(Date.now()-i>=o){h(!1);return}setTimeout(n,a)};n()}))return this.logger.warn("Extension discovery timed out"),this.setState(s.EXTENSION_STATE_NOT_FOUND),this.state}const t=await this.bodhiext.getExtensionId();this.logger.info(`Extension discovered: ${t}`);const r={type:"extension",extension:"ready",extensionId:t,server:s.PENDING_EXTENSION_READY};if(e.testConnection)try{const o=await this.getServerState();this.setState({...r,server:o}),this.logger.info(`Server connectivity tested: ${o.status}`)}catch(o){this.logger.error("Failed to get server state:",o),this.setState({...r,server:s.BACKEND_SERVER_NOT_REACHABLE})}else this.setState(r);return this.state}async requestResourceAccess(){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],c=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}?${c}`;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 n=await this._getAccessTokenRaw();if(!n)throw s.createOperationError("Not authenticated. Please log in first.","auth_error");i={...i,Authorization:`Bearer ${n}`}}const h=this.bodhiext.sendStreamRequest(e,t,r,i).getReader();try{for(;;){const{value:n,done:d}=await h.read();if(d||n?.done)break;yield n.body}}catch(n){if(n instanceof Error){if("response"in n){const d=n;throw s.createApiError(n.message,d.response.status,d.response.body)}throw"error"in n,s.createOperationError(n.message,"extension_error")}throw n}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 _ 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 f(this.authClientId,e,t,e.basePath)}createDirectClient(e,t,r){return new g({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 p="production";exports.WEB_BUILD_MODE=p;exports.WebUIClient=_;
|
package/dist/bodhi-web.esm.d.ts
CHANGED
package/dist/bodhi-web.esm.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { DirectClientBase as
|
|
2
|
-
class
|
|
1
|
+
import { DirectClientBase as R, createStoragePrefixWithBasePath as y, STORAGE_PREFIXES as S, generateCodeVerifier as u, generateCodeChallenge as _, isApiResultOperationError as w, isApiResultSuccess as f, createStorageKeys as I, EXTENSION_STATE_NOT_INITIALIZED as p, Logger as T, createOAuthEndpoints as C, NOOP_STATE_CALLBACK as x, createOperationError as h, EXTENSION_STATE_NOT_FOUND as A, PENDING_EXTENSION_READY as k, BACKEND_SERVER_NOT_REACHABLE as g, extractUserInfo as m, refreshAccessToken as v, backendServerNotReady as E, SERVER_ERROR_CODES as b, createApiError as O, BaseFacadeClient as P } from "@bodhiapp/bodhi-js-core";
|
|
2
|
+
class K extends R {
|
|
3
3
|
constructor(e, t) {
|
|
4
|
-
const
|
|
5
|
-
super({ ...e, storagePrefix:
|
|
4
|
+
const r = e.basePath || "/", s = y(r, S.DIRECT);
|
|
5
|
+
super({ ...e, storagePrefix: s }, "DirectWebClient", t), this.redirectUri = e.redirectUri;
|
|
6
6
|
}
|
|
7
7
|
// ============================================================================
|
|
8
8
|
// Authentication (Browser Redirect OAuth)
|
|
@@ -11,27 +11,27 @@ class L extends x {
|
|
|
11
11
|
const e = await this.getAuthState();
|
|
12
12
|
if (e.isLoggedIn)
|
|
13
13
|
return e;
|
|
14
|
-
const t = await this.requestResourceAccess(),
|
|
15
|
-
localStorage.setItem(this.storageKeys.CODE_VERIFIER,
|
|
16
|
-
const
|
|
17
|
-
throw
|
|
14
|
+
const t = await this.requestResourceAccess(), r = `openid profile email roles ${this.userScope} ${t}`, s = u(), i = await _(s), o = u();
|
|
15
|
+
localStorage.setItem(this.storageKeys.CODE_VERIFIER, s), localStorage.setItem(this.storageKeys.STATE, o);
|
|
16
|
+
const n = new URL(this.authEndpoints.authorize);
|
|
17
|
+
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", i), n.searchParams.set("code_challenge_method", "S256"), n.searchParams.set("state", o), window.location.href = n.toString(), new Error("Redirect initiated");
|
|
18
18
|
}
|
|
19
19
|
async handleOAuthCallback(e, t) {
|
|
20
|
-
const
|
|
21
|
-
if (!
|
|
20
|
+
const r = localStorage.getItem(this.storageKeys.STATE);
|
|
21
|
+
if (!r || r !== t)
|
|
22
22
|
throw new Error("Invalid state parameter - possible CSRF attack");
|
|
23
23
|
await this.exchangeCodeForTokens(e), localStorage.removeItem(this.storageKeys.CODE_VERIFIER), localStorage.removeItem(this.storageKeys.STATE);
|
|
24
|
-
const
|
|
25
|
-
if (!
|
|
24
|
+
const s = await this.getAuthState();
|
|
25
|
+
if (!s.isLoggedIn)
|
|
26
26
|
throw new Error("Login failed");
|
|
27
|
-
const i =
|
|
27
|
+
const i = s;
|
|
28
28
|
return this.setAuthState(i), i;
|
|
29
29
|
}
|
|
30
30
|
async logout() {
|
|
31
31
|
const e = localStorage.getItem(this.storageKeys.REFRESH_TOKEN);
|
|
32
32
|
if (e)
|
|
33
33
|
try {
|
|
34
|
-
const
|
|
34
|
+
const r = new URLSearchParams({
|
|
35
35
|
token: e,
|
|
36
36
|
client_id: this.authClientId,
|
|
37
37
|
token_type_hint: "refresh_token"
|
|
@@ -41,10 +41,10 @@ class L extends x {
|
|
|
41
41
|
headers: {
|
|
42
42
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
43
43
|
},
|
|
44
|
-
body:
|
|
44
|
+
body: r
|
|
45
45
|
});
|
|
46
|
-
} catch (
|
|
47
|
-
this.logger.warn("Token revocation failed:",
|
|
46
|
+
} catch (r) {
|
|
47
|
+
this.logger.warn("Token revocation failed:", r);
|
|
48
48
|
}
|
|
49
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);
|
|
50
50
|
const t = {
|
|
@@ -63,7 +63,7 @@ class L extends x {
|
|
|
63
63
|
{},
|
|
64
64
|
!1
|
|
65
65
|
);
|
|
66
|
-
if (
|
|
66
|
+
if (w(e))
|
|
67
67
|
throw new Error("Failed to get resource access scope from server");
|
|
68
68
|
if (!f(e))
|
|
69
69
|
throw new Error("Failed to get resource access scope from server: API error");
|
|
@@ -74,7 +74,7 @@ class L extends x {
|
|
|
74
74
|
const t = localStorage.getItem(this.storageKeys.CODE_VERIFIER);
|
|
75
75
|
if (!t)
|
|
76
76
|
throw new Error("Code verifier not found");
|
|
77
|
-
const
|
|
77
|
+
const r = await fetch(this.authEndpoints.token, {
|
|
78
78
|
method: "POST",
|
|
79
79
|
headers: {
|
|
80
80
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
@@ -87,13 +87,13 @@ class L extends x {
|
|
|
87
87
|
code_verifier: t
|
|
88
88
|
})
|
|
89
89
|
});
|
|
90
|
-
if (!
|
|
91
|
-
const i = await
|
|
92
|
-
throw new Error(`Token exchange failed: ${
|
|
90
|
+
if (!r.ok) {
|
|
91
|
+
const i = await r.text();
|
|
92
|
+
throw new Error(`Token exchange failed: ${r.status} ${i}`);
|
|
93
93
|
}
|
|
94
|
-
const
|
|
95
|
-
if (localStorage.setItem(this.storageKeys.ACCESS_TOKEN,
|
|
96
|
-
const i = Date.now() +
|
|
94
|
+
const s = await r.json();
|
|
95
|
+
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) {
|
|
96
|
+
const i = Date.now() + s.expires_in * 1e3;
|
|
97
97
|
localStorage.setItem(this.storageKeys.EXPIRES_AT, i.toString());
|
|
98
98
|
}
|
|
99
99
|
}
|
|
@@ -104,8 +104,8 @@ class L extends x {
|
|
|
104
104
|
return localStorage.getItem(e);
|
|
105
105
|
}
|
|
106
106
|
async _storageSet(e) {
|
|
107
|
-
Object.entries(e).forEach(([t,
|
|
108
|
-
localStorage.setItem(t, String(
|
|
107
|
+
Object.entries(e).forEach(([t, r]) => {
|
|
108
|
+
localStorage.setItem(t, String(r));
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
async _storageRemove(e) {
|
|
@@ -115,15 +115,15 @@ class L extends x {
|
|
|
115
115
|
return this.redirectUri;
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
function
|
|
121
|
-
const e =
|
|
122
|
-
return
|
|
118
|
+
const U = 500, L = 5e3;
|
|
119
|
+
I(S.WEB);
|
|
120
|
+
function N(l = "/") {
|
|
121
|
+
const e = y(l, S.WEB);
|
|
122
|
+
return I(e);
|
|
123
123
|
}
|
|
124
|
-
class
|
|
125
|
-
constructor(e, t,
|
|
126
|
-
this.state =
|
|
124
|
+
class D {
|
|
125
|
+
constructor(e, t, r, s) {
|
|
126
|
+
this.state = p, this.bodhiext = null, this.refreshPromise = null, this.logger = new T("WindowBodhiextClient", t.logLevel), this.authClientId = e, this.config = t, this.authEndpoints = C(this.config.authServerUrl), this.onStateChange = r ?? x, this.storageKeys = N(s || "/");
|
|
127
127
|
}
|
|
128
128
|
/**
|
|
129
129
|
* Set client state and notify callback
|
|
@@ -148,11 +148,11 @@ class B {
|
|
|
148
148
|
// ============================================================================
|
|
149
149
|
/**
|
|
150
150
|
* Ensure bodhiext is available, attempting to acquire it if not already set
|
|
151
|
-
* @throws
|
|
151
|
+
* @throws OperationError if client not initialized
|
|
152
152
|
*/
|
|
153
153
|
ensureBodhiext() {
|
|
154
154
|
if (!this.bodhiext && window.bodhiext && (this.logger.info("Acquiring window.bodhiext reference"), this.bodhiext = window.bodhiext), !this.bodhiext)
|
|
155
|
-
throw
|
|
155
|
+
throw h("Client not initialized", "extension_error");
|
|
156
156
|
}
|
|
157
157
|
/**
|
|
158
158
|
* Send extension request via window.bodhiext.sendExtRequest
|
|
@@ -164,7 +164,7 @@ class B {
|
|
|
164
164
|
* Send API message via window.bodhiext.sendApiRequest
|
|
165
165
|
* Converts ApiResponse to ApiResponseResult
|
|
166
166
|
*/
|
|
167
|
-
async sendApiRequest(e, t,
|
|
167
|
+
async sendApiRequest(e, t, r, s, i) {
|
|
168
168
|
try {
|
|
169
169
|
this.ensureBodhiext();
|
|
170
170
|
} catch (o) {
|
|
@@ -176,7 +176,7 @@ class B {
|
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
178
|
try {
|
|
179
|
-
let o =
|
|
179
|
+
let o = s || {};
|
|
180
180
|
if (i) {
|
|
181
181
|
const c = await this._getAccessTokenRaw();
|
|
182
182
|
if (!c)
|
|
@@ -194,15 +194,15 @@ class B {
|
|
|
194
194
|
return await this.bodhiext.sendApiRequest(
|
|
195
195
|
e,
|
|
196
196
|
t,
|
|
197
|
-
|
|
197
|
+
r,
|
|
198
198
|
o
|
|
199
199
|
);
|
|
200
200
|
} catch (o) {
|
|
201
|
-
const
|
|
201
|
+
const n = o?.error, c = n?.message ?? (o instanceof Error ? o.message : String(o)), a = n?.type || "extension_error";
|
|
202
202
|
return {
|
|
203
203
|
error: {
|
|
204
204
|
message: c,
|
|
205
|
-
type:
|
|
205
|
+
type: a
|
|
206
206
|
}
|
|
207
207
|
};
|
|
208
208
|
}
|
|
@@ -227,46 +227,45 @@ class B {
|
|
|
227
227
|
* No extensionId storage/restoration needed - window.bodhiext handle is ephemeral
|
|
228
228
|
*/
|
|
229
229
|
async init(e = {}) {
|
|
230
|
-
var r, i, o, a;
|
|
231
230
|
if (!e.testConnection && !e.selectedConnection)
|
|
232
|
-
return this.logger.info("No testConnection or selectedConnection, returning not-initialized state"),
|
|
231
|
+
return this.logger.info("No testConnection or selectedConnection, returning not-initialized state"), p;
|
|
233
232
|
if (this.bodhiext && !e.testConnection)
|
|
234
233
|
return this.logger.debug("Already have bodhiext handle, skipping polling"), this.state;
|
|
235
234
|
if (!this.bodhiext) {
|
|
236
|
-
const
|
|
237
|
-
if (!await new Promise((
|
|
238
|
-
const
|
|
235
|
+
const s = e.timeoutMs ?? this.config.initParams?.extension?.timeoutMs ?? L, i = e.intervalMs ?? this.config.initParams?.extension?.intervalMs ?? U, o = Date.now();
|
|
236
|
+
if (!await new Promise((c) => {
|
|
237
|
+
const a = () => {
|
|
239
238
|
if (window.bodhiext) {
|
|
240
|
-
this.bodhiext = window.bodhiext,
|
|
239
|
+
this.bodhiext = window.bodhiext, c(!0);
|
|
241
240
|
return;
|
|
242
241
|
}
|
|
243
|
-
if (Date.now() -
|
|
244
|
-
|
|
242
|
+
if (Date.now() - o >= s) {
|
|
243
|
+
c(!1);
|
|
245
244
|
return;
|
|
246
245
|
}
|
|
247
|
-
setTimeout(
|
|
246
|
+
setTimeout(a, i);
|
|
248
247
|
};
|
|
249
|
-
|
|
248
|
+
a();
|
|
250
249
|
}))
|
|
251
|
-
return this.logger.warn("Extension discovery timed out"), this.setState(
|
|
250
|
+
return this.logger.warn("Extension discovery timed out"), this.setState(A), this.state;
|
|
252
251
|
}
|
|
253
252
|
const t = await this.bodhiext.getExtensionId();
|
|
254
253
|
this.logger.info(`Extension discovered: ${t}`);
|
|
255
|
-
const
|
|
254
|
+
const r = {
|
|
256
255
|
type: "extension",
|
|
257
256
|
extension: "ready",
|
|
258
257
|
extensionId: t,
|
|
259
|
-
server:
|
|
258
|
+
server: k
|
|
260
259
|
};
|
|
261
260
|
if (e.testConnection)
|
|
262
261
|
try {
|
|
263
|
-
const
|
|
264
|
-
this.setState({ ...
|
|
265
|
-
} catch (
|
|
266
|
-
this.logger.error("Failed to get server state:",
|
|
262
|
+
const s = await this.getServerState();
|
|
263
|
+
this.setState({ ...r, server: s }), this.logger.info(`Server connectivity tested: ${s.status}`);
|
|
264
|
+
} catch (s) {
|
|
265
|
+
this.logger.error("Failed to get server state:", s), this.setState({ ...r, server: g });
|
|
267
266
|
}
|
|
268
267
|
else
|
|
269
|
-
this.setState(
|
|
268
|
+
this.setState(r);
|
|
270
269
|
return this.state;
|
|
271
270
|
}
|
|
272
271
|
// ============================================================================
|
|
@@ -295,17 +294,17 @@ class B {
|
|
|
295
294
|
if (e.isLoggedIn)
|
|
296
295
|
return e;
|
|
297
296
|
this.ensureBodhiext();
|
|
298
|
-
const t = await this.requestResourceAccess(),
|
|
299
|
-
localStorage.setItem(this.storageKeys.CODE_VERIFIER,
|
|
300
|
-
const o = ["openid", "profile", "email", "roles", this.config.userScope, t],
|
|
297
|
+
const t = await this.requestResourceAccess(), r = u(), s = await _(r), i = u();
|
|
298
|
+
localStorage.setItem(this.storageKeys.CODE_VERIFIER, r), localStorage.setItem(this.storageKeys.STATE, i);
|
|
299
|
+
const o = ["openid", "profile", "email", "roles", this.config.userScope, t], n = new URLSearchParams({
|
|
301
300
|
response_type: "code",
|
|
302
301
|
client_id: this.authClientId,
|
|
303
302
|
redirect_uri: this.config.redirectUri,
|
|
304
303
|
scope: o.join(" "),
|
|
305
304
|
state: i,
|
|
306
|
-
code_challenge:
|
|
305
|
+
code_challenge: s,
|
|
307
306
|
code_challenge_method: "S256"
|
|
308
|
-
}), c = `${this.authEndpoints.authorize}?${
|
|
307
|
+
}), c = `${this.authEndpoints.authorize}?${n}`;
|
|
309
308
|
return window.location.href = c, new Promise(() => {
|
|
310
309
|
});
|
|
311
310
|
}
|
|
@@ -315,14 +314,14 @@ class B {
|
|
|
315
314
|
* @returns AuthLoggedIn with login state and user info
|
|
316
315
|
*/
|
|
317
316
|
async handleOAuthCallback(e, t) {
|
|
318
|
-
const
|
|
319
|
-
if (!
|
|
317
|
+
const r = localStorage.getItem(this.storageKeys.STATE);
|
|
318
|
+
if (!r || r !== t)
|
|
320
319
|
throw new Error("Invalid state parameter - possible CSRF attack");
|
|
321
320
|
await this.exchangeCodeForTokens(e), localStorage.removeItem(this.storageKeys.CODE_VERIFIER), localStorage.removeItem(this.storageKeys.STATE);
|
|
322
|
-
const
|
|
323
|
-
if (!
|
|
321
|
+
const s = await this.getAuthState();
|
|
322
|
+
if (!s.isLoggedIn)
|
|
324
323
|
throw new Error("Login failed");
|
|
325
|
-
return this.setAuthState(
|
|
324
|
+
return this.setAuthState(s), s;
|
|
326
325
|
}
|
|
327
326
|
/**
|
|
328
327
|
* Exchange authorization code for tokens
|
|
@@ -331,24 +330,24 @@ class B {
|
|
|
331
330
|
const t = localStorage.getItem(this.storageKeys.CODE_VERIFIER);
|
|
332
331
|
if (!t)
|
|
333
332
|
throw new Error("Code verifier not found");
|
|
334
|
-
const
|
|
333
|
+
const r = new URLSearchParams({
|
|
335
334
|
grant_type: "authorization_code",
|
|
336
335
|
client_id: this.authClientId,
|
|
337
336
|
code: e,
|
|
338
337
|
redirect_uri: this.config.redirectUri,
|
|
339
338
|
code_verifier: t
|
|
340
|
-
}),
|
|
339
|
+
}), s = await fetch(this.authEndpoints.token, {
|
|
341
340
|
method: "POST",
|
|
342
341
|
headers: {
|
|
343
342
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
344
343
|
},
|
|
345
|
-
body:
|
|
344
|
+
body: r
|
|
346
345
|
});
|
|
347
|
-
if (!
|
|
348
|
-
const o = await
|
|
349
|
-
throw new Error(`Token exchange failed: ${
|
|
346
|
+
if (!s.ok) {
|
|
347
|
+
const o = await s.text();
|
|
348
|
+
throw new Error(`Token exchange failed: ${s.status} ${o}`);
|
|
350
349
|
}
|
|
351
|
-
const i = await
|
|
350
|
+
const i = await s.json();
|
|
352
351
|
if (!i.access_token)
|
|
353
352
|
throw new Error("No access token received");
|
|
354
353
|
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) {
|
|
@@ -364,7 +363,7 @@ class B {
|
|
|
364
363
|
const e = localStorage.getItem(this.storageKeys.REFRESH_TOKEN);
|
|
365
364
|
if (e)
|
|
366
365
|
try {
|
|
367
|
-
const
|
|
366
|
+
const r = new URLSearchParams({
|
|
368
367
|
token: e,
|
|
369
368
|
client_id: this.authClientId,
|
|
370
369
|
token_type_hint: "refresh_token"
|
|
@@ -374,10 +373,10 @@ class B {
|
|
|
374
373
|
headers: {
|
|
375
374
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
376
375
|
},
|
|
377
|
-
body:
|
|
376
|
+
body: r
|
|
378
377
|
});
|
|
379
|
-
} catch (
|
|
380
|
-
this.logger.warn("Token revocation failed:",
|
|
378
|
+
} catch (r) {
|
|
379
|
+
this.logger.warn("Token revocation failed:", r);
|
|
381
380
|
}
|
|
382
381
|
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);
|
|
383
382
|
const t = {
|
|
@@ -393,7 +392,7 @@ class B {
|
|
|
393
392
|
if (!e)
|
|
394
393
|
return { isLoggedIn: !1 };
|
|
395
394
|
try {
|
|
396
|
-
return { isLoggedIn: !0, userInfo:
|
|
395
|
+
return { isLoggedIn: !0, userInfo: m(e), accessToken: e };
|
|
397
396
|
} catch (t) {
|
|
398
397
|
return this.logger.error("Failed to parse token:", t), { isLoggedIn: !1 };
|
|
399
398
|
}
|
|
@@ -407,10 +406,10 @@ class B {
|
|
|
407
406
|
if (!e)
|
|
408
407
|
return null;
|
|
409
408
|
if (t) {
|
|
410
|
-
const
|
|
411
|
-
if (Date.now() >=
|
|
412
|
-
const
|
|
413
|
-
return
|
|
409
|
+
const r = parseInt(t, 10);
|
|
410
|
+
if (Date.now() >= r - 5 * 1e3) {
|
|
411
|
+
const s = localStorage.getItem(this.storageKeys.REFRESH_TOKEN);
|
|
412
|
+
return s ? this._tryRefreshToken(s) : null;
|
|
414
413
|
}
|
|
415
414
|
}
|
|
416
415
|
return e;
|
|
@@ -435,24 +434,24 @@ class B {
|
|
|
435
434
|
async _doRefreshToken(e) {
|
|
436
435
|
this.logger.debug("Refreshing access token");
|
|
437
436
|
try {
|
|
438
|
-
const t = await
|
|
437
|
+
const t = await v(
|
|
439
438
|
this.authEndpoints.token,
|
|
440
439
|
e,
|
|
441
440
|
this.authClientId
|
|
442
441
|
);
|
|
443
442
|
if (t) {
|
|
444
443
|
this._storeRefreshedTokens(t);
|
|
445
|
-
const
|
|
444
|
+
const r = m(t.access_token);
|
|
446
445
|
return this.setAuthState({
|
|
447
446
|
isLoggedIn: !0,
|
|
448
|
-
userInfo:
|
|
447
|
+
userInfo: r,
|
|
449
448
|
accessToken: t.access_token
|
|
450
449
|
}), this.logger.info("Token refreshed successfully"), t.access_token;
|
|
451
450
|
}
|
|
452
451
|
} catch (t) {
|
|
453
452
|
this.logger.warn("Token refresh failed:", t);
|
|
454
453
|
}
|
|
455
|
-
throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"),
|
|
454
|
+
throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"), h(
|
|
456
455
|
"Access token expired and unable to refresh. Try logging out and logging in again.",
|
|
457
456
|
"token_refresh_failed"
|
|
458
457
|
);
|
|
@@ -488,10 +487,10 @@ class B {
|
|
|
488
487
|
*/
|
|
489
488
|
async getServerState() {
|
|
490
489
|
const e = await this.sendApiRequest("GET", "/bodhi/v1/info");
|
|
491
|
-
if (
|
|
492
|
-
return
|
|
490
|
+
if (w(e))
|
|
491
|
+
return g;
|
|
493
492
|
if (!f(e))
|
|
494
|
-
return
|
|
493
|
+
return g;
|
|
495
494
|
const t = e.body;
|
|
496
495
|
switch (t.status) {
|
|
497
496
|
case "ready":
|
|
@@ -504,45 +503,45 @@ class B {
|
|
|
504
503
|
return E(
|
|
505
504
|
"error",
|
|
506
505
|
t.version || "unknown",
|
|
507
|
-
t.error ? { message: t.error.message, type: t.error.type } :
|
|
506
|
+
t.error ? { message: t.error.message, type: t.error.type } : b.SERVER_NOT_READY
|
|
508
507
|
);
|
|
509
508
|
default:
|
|
510
|
-
return
|
|
509
|
+
return g;
|
|
511
510
|
}
|
|
512
511
|
}
|
|
513
512
|
/**
|
|
514
513
|
* Generic streaming via window.bodhiext.sendStreamRequest
|
|
515
514
|
* Wraps ReadableStream as AsyncGenerator
|
|
516
515
|
*/
|
|
517
|
-
async *stream(e, t,
|
|
516
|
+
async *stream(e, t, r, s, i = !0) {
|
|
518
517
|
this.ensureBodhiext();
|
|
519
|
-
let o =
|
|
518
|
+
let o = s || {};
|
|
520
519
|
if (i) {
|
|
521
|
-
const
|
|
522
|
-
if (!
|
|
523
|
-
throw
|
|
520
|
+
const a = await this._getAccessTokenRaw();
|
|
521
|
+
if (!a)
|
|
522
|
+
throw h("Not authenticated. Please log in first.", "auth_error");
|
|
524
523
|
o = {
|
|
525
524
|
...o,
|
|
526
|
-
Authorization: `Bearer ${
|
|
525
|
+
Authorization: `Bearer ${a}`
|
|
527
526
|
};
|
|
528
527
|
}
|
|
529
|
-
const c = this.bodhiext.sendStreamRequest(e, t,
|
|
528
|
+
const c = this.bodhiext.sendStreamRequest(e, t, r, o).getReader();
|
|
530
529
|
try {
|
|
531
530
|
for (; ; ) {
|
|
532
|
-
const { value:
|
|
533
|
-
if (
|
|
531
|
+
const { value: a, done: d } = await c.read();
|
|
532
|
+
if (d || a?.done)
|
|
534
533
|
break;
|
|
535
|
-
yield
|
|
534
|
+
yield a.body;
|
|
536
535
|
}
|
|
537
|
-
} catch (
|
|
538
|
-
if (
|
|
539
|
-
if ("response" in
|
|
540
|
-
const
|
|
541
|
-
throw
|
|
536
|
+
} catch (a) {
|
|
537
|
+
if (a instanceof Error) {
|
|
538
|
+
if ("response" in a) {
|
|
539
|
+
const d = a;
|
|
540
|
+
throw O(a.message, d.response.status, d.response.body);
|
|
542
541
|
}
|
|
543
|
-
throw "error" in
|
|
542
|
+
throw "error" in a ? h(a.message, "extension_error") : h(a.message, "extension_error");
|
|
544
543
|
}
|
|
545
|
-
throw
|
|
544
|
+
throw a;
|
|
546
545
|
} finally {
|
|
547
546
|
c.releaseLock();
|
|
548
547
|
}
|
|
@@ -550,7 +549,7 @@ class B {
|
|
|
550
549
|
/**
|
|
551
550
|
* Chat streaming
|
|
552
551
|
*/
|
|
553
|
-
async *streamChat(e, t,
|
|
552
|
+
async *streamChat(e, t, r = !0) {
|
|
554
553
|
yield* this.stream(
|
|
555
554
|
"POST",
|
|
556
555
|
"/v1/chat/completions",
|
|
@@ -560,7 +559,7 @@ class B {
|
|
|
560
559
|
stream: !0
|
|
561
560
|
},
|
|
562
561
|
void 0,
|
|
563
|
-
|
|
562
|
+
r
|
|
564
563
|
);
|
|
565
564
|
}
|
|
566
565
|
/**
|
|
@@ -587,8 +586,8 @@ class B {
|
|
|
587
586
|
};
|
|
588
587
|
}
|
|
589
588
|
}
|
|
590
|
-
class
|
|
591
|
-
constructor(e, t,
|
|
589
|
+
class B extends P {
|
|
590
|
+
constructor(e, t, r, s) {
|
|
592
591
|
const i = {
|
|
593
592
|
redirectUri: t.redirectUri,
|
|
594
593
|
authServerUrl: t.authServerUrl || "https://id.getbodhi.app/realms/bodhi",
|
|
@@ -597,26 +596,26 @@ class W extends U {
|
|
|
597
596
|
logLevel: t.logLevel || "warn",
|
|
598
597
|
initParams: t.initParams
|
|
599
598
|
};
|
|
600
|
-
super(e, i,
|
|
599
|
+
super(e, i, r, s, t.basePath);
|
|
601
600
|
}
|
|
602
601
|
createLogger(e) {
|
|
603
|
-
return new
|
|
602
|
+
return new T("WebUIClient", e.logLevel);
|
|
604
603
|
}
|
|
605
604
|
createExtClient(e, t) {
|
|
606
|
-
return new
|
|
605
|
+
return new D(this.authClientId, e, t, e.basePath);
|
|
607
606
|
}
|
|
608
|
-
createDirectClient(e, t,
|
|
609
|
-
return new
|
|
607
|
+
createDirectClient(e, t, r) {
|
|
608
|
+
return new K(
|
|
610
609
|
{
|
|
611
610
|
authClientId: e,
|
|
612
611
|
authServerUrl: t.authServerUrl,
|
|
613
612
|
redirectUri: t.redirectUri,
|
|
614
613
|
userScope: t.userScope,
|
|
615
614
|
logLevel: t.logLevel,
|
|
616
|
-
storagePrefix:
|
|
615
|
+
storagePrefix: S.WEB,
|
|
617
616
|
basePath: t.basePath
|
|
618
617
|
},
|
|
619
|
-
|
|
618
|
+
r
|
|
620
619
|
);
|
|
621
620
|
}
|
|
622
621
|
// ============================================================================
|
|
@@ -626,8 +625,8 @@ class W extends U {
|
|
|
626
625
|
return this.connectionMode === "direct" ? this.directClient.handleOAuthCallback(e, t) : this.extClient.handleOAuthCallback(e, t);
|
|
627
626
|
}
|
|
628
627
|
}
|
|
629
|
-
const
|
|
628
|
+
const q = "production";
|
|
630
629
|
export {
|
|
631
|
-
|
|
632
|
-
|
|
630
|
+
q as WEB_BUILD_MODE,
|
|
631
|
+
B as WebUIClient
|
|
633
632
|
};
|
|
@@ -2,7 +2,6 @@ import { Browser, EnvState, OS } from './platform';
|
|
|
2
2
|
import { ExtensionState } from './extension';
|
|
3
3
|
import { ServerState } from './server';
|
|
4
4
|
import { LnaServerState, LnaState } from './lna';
|
|
5
|
-
|
|
6
5
|
export declare enum SetupStep {
|
|
7
6
|
PLATFORM_CHECK = "platform-check",
|
|
8
7
|
SERVER_SETUP = "server-setup",
|
|
@@ -2,7 +2,6 @@ import { ExtensionState, ExtensionStateNotReady, ExtensionStateReady } from './e
|
|
|
2
2
|
import { ServerState, ServerStateError, ServerStatePending, ServerStateReachable, ServerStateReady, ServerStateUnreachable } from './server';
|
|
3
3
|
import { LnaServerState, LnaServerStateError, LnaServerStatePending, LnaServerStateReady, LnaServerStateResourceAdmin, LnaServerStateSetup, LnaState, LnaStateDenied, LnaStateGranted, LnaStatePrompt, LnaStateSkipped, LnaStateUnreachable, LnaStateUnsupported } from './lna';
|
|
4
4
|
import { Browser, NotSupportedBrowser, NotSupportedOS, OS, SupportedBrowser, SupportedOS } from './platform';
|
|
5
|
-
|
|
6
5
|
export declare function isExtensionStateReady(ext: ExtensionState): ext is ExtensionStateReady;
|
|
7
6
|
export declare function isExtensionStateNotReady(ext: ExtensionState): ext is ExtensionStateNotReady;
|
|
8
7
|
export declare function isServerStateReady(server: ServerState): server is ServerStateReady;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bodhiapp/bodhi-js",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Web SDK for Bodhi Browser - window.bodhiext communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/bodhi-web.cjs.js",
|
|
@@ -34,11 +34,12 @@
|
|
|
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.
|
|
38
|
-
"@bodhiapp/ts-client": "
|
|
37
|
+
"@bodhiapp/bodhi-js-core": "0.0.5",
|
|
38
|
+
"@bodhiapp/ts-client": "0.1.8"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@eslint/js": "^9.23.0",
|
|
42
|
+
"@types/node": "^20.19.10",
|
|
42
43
|
"@typescript-eslint/eslint-plugin": "8.28.0",
|
|
43
44
|
"@typescript-eslint/parser": "8.28.0",
|
|
44
45
|
"eslint": "9.32.0",
|
|
@@ -48,7 +49,7 @@
|
|
|
48
49
|
"rimraf": "^6.0.1",
|
|
49
50
|
"tslib": "^2.6.2",
|
|
50
51
|
"typescript": "^5.8.3",
|
|
51
|
-
"vite": "^
|
|
52
|
-
"vite-plugin-dts": "^
|
|
52
|
+
"vite": "^7.1.12",
|
|
53
|
+
"vite-plugin-dts": "^4.5.4"
|
|
53
54
|
}
|
|
54
55
|
}
|