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