@bodhiapp/bodhi-js 0.0.3 → 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.
Files changed (33) hide show
  1. package/dist/bodhi-browser-ext/src/types/bodhiext.d.ts +0 -1
  2. package/dist/bodhi-browser-ext/src/types/protocol.d.ts +0 -1
  3. package/dist/bodhi-js-sdk/core/src/build-info.d.ts +1 -0
  4. package/dist/bodhi-js-sdk/core/src/errors.d.ts +0 -1
  5. package/dist/bodhi-js-sdk/core/src/facade-client-base.d.ts +1 -2
  6. package/dist/bodhi-js-sdk/core/src/index.d.ts +1 -0
  7. package/dist/bodhi-js-sdk/core/src/interface.d.ts +0 -1
  8. package/dist/bodhi-js-sdk/core/src/logger.d.ts +0 -1
  9. package/dist/bodhi-js-sdk/core/src/oauth.d.ts +0 -1
  10. package/dist/bodhi-js-sdk/core/src/onboarding/config.d.ts +0 -1
  11. package/dist/bodhi-js-sdk/core/src/onboarding/modal.d.ts +0 -1
  12. package/dist/bodhi-js-sdk/core/src/onboarding/protocol-utils.d.ts +0 -1
  13. package/dist/bodhi-js-sdk/core/src/platform.d.ts +0 -1
  14. package/dist/bodhi-js-sdk/core/src/storage.d.ts +12 -0
  15. package/dist/bodhi-js-sdk/core/src/types/api.d.ts +0 -1
  16. package/dist/bodhi-js-sdk/core/src/types/callback.d.ts +0 -1
  17. package/dist/bodhi-js-sdk/core/src/types/client-state.d.ts +0 -1
  18. package/dist/bodhi-js-sdk/core/src/types/config.d.ts +0 -1
  19. package/dist/bodhi-js-sdk/core/src/types/platform.d.ts +0 -1
  20. package/dist/bodhi-js-sdk/web/src/build-info.d.ts +1 -0
  21. package/dist/bodhi-js-sdk/web/src/constants.d.ts +14 -4
  22. package/dist/bodhi-js-sdk/web/src/direct-client.d.ts +1 -1
  23. package/dist/bodhi-js-sdk/web/src/ext-client.d.ts +3 -3
  24. package/dist/bodhi-js-sdk/web/src/facade-client.d.ts +2 -1
  25. package/dist/bodhi-js-sdk/web/src/index.d.ts +1 -1
  26. package/dist/bodhi-web.cjs.js +1 -1
  27. package/dist/bodhi-web.esm.d.ts +1 -0
  28. package/dist/bodhi-web.esm.js +97 -88
  29. package/dist/setup-modal/src/types/message-types.d.ts +0 -1
  30. package/dist/setup-modal/src/types/protocol.d.ts +0 -1
  31. package/dist/setup-modal/src/types/state.d.ts +0 -1
  32. package/dist/setup-modal/src/types/type-guards.d.ts +0 -1
  33. 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
@@ -1,6 +1,5 @@
1
1
  import { OpenAiApiError, ErrorBody } from '@bodhiapp/ts-client';
2
2
  import { ApiResponse, ServerStateInfo } from './bodhiext';
3
-
4
3
  /**
5
4
  * Validate OpenAI API error body structure
6
5
  * { error: { message: string, type: string } }
@@ -0,0 +1 @@
1
+ export declare const BUILD_MODE: string;
@@ -1,6 +1,5 @@
1
1
  import { ApiError, OperationError } from '../../../bodhi-browser-ext/src/types';
2
2
  import { OpenAiApiError } from '@bodhiapp/ts-client';
3
-
4
3
  /**
5
4
  * Create API error (HTTP 4xx/5xx from server)
6
5
  * Thrown for streaming responses when server returns error
@@ -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
  *
@@ -20,7 +19,7 @@ export declare abstract class BaseFacadeClient<TConfig, TExtClient extends IConn
20
19
  protected authClientId: string;
21
20
  protected config: TConfig;
22
21
  protected onStateChange: StateChangeCallback;
23
- constructor(authClientId: string, config: TConfig, onStateChange?: StateChangeCallback, storagePrefix?: string);
22
+ constructor(authClientId: string, config: TConfig, onStateChange?: StateChangeCallback, storagePrefix?: string, basePath?: string);
24
23
  /**
25
24
  * Create logger instance
26
25
  * Subclasses extract logLevel from their specific config type
@@ -17,3 +17,4 @@ export * from './oauth';
17
17
  export * from './direct-client-base';
18
18
  export * from './facade-client-base';
19
19
  export { isOperationError, type OperationError } from '../../../bodhi-browser-ext/src/types';
20
+ export { BUILD_MODE as CORE_BUILD_MODE } from './build-info';
@@ -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 { LogLevel } from './types/config';
2
-
3
2
  export declare class Logger {
4
3
  private prefix;
5
4
  private level;
@@ -1,5 +1,4 @@
1
1
  import { UserInfo } from './types';
2
-
3
2
  /**
4
3
  * Base64 URL encode a buffer (for PKCE)
5
4
  */
@@ -1,5 +1,4 @@
1
1
  import { Browser, OS } from '../../../../setup-modal/src/types';
2
-
3
2
  /**
4
3
  * Browser configurations with extension store URLs
5
4
  */
@@ -1,5 +1,4 @@
1
1
  import { MessageType, RequestMessage, ResponsePayload } from '../../../../setup-modal/src/types';
2
-
3
2
  import type * as ModalTypes from '@bodhiapp/setup-modal/types';
4
3
  /**
5
4
  * Async handler for modal requests
@@ -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,5 +1,4 @@
1
1
  import { BrowserInfo, OSInfo } from './types';
2
-
3
2
  /**
4
3
  * Detects the current browser using UAParser.js
5
4
  */
@@ -27,6 +27,18 @@ export declare const STORAGE_PREFIXES: {
27
27
  readonly WEB: "bodhi:web";
28
28
  readonly EXT: "bodhi:ext";
29
29
  };
30
+ /**
31
+ * Create storage prefix with basePath for path isolation
32
+ *
33
+ * @param basePath - Base path of app (e.g., '/', '/app1/')
34
+ * @param prefix - Storage prefix (e.g., 'bodhi:web', 'bodhijs:')
35
+ * @returns Combined prefix with basePath isolation
36
+ *
37
+ * Examples:
38
+ * - createStoragePrefixWithBasePath('/', 'bodhi:web') => '/:bodhi:web'
39
+ * - createStoragePrefixWithBasePath('/app1/', 'bodhi:web') => '/app1/:bodhi:web'
40
+ */
41
+ export declare function createStoragePrefixWithBasePath(basePath: string, prefix: string): string;
30
42
  /**
31
43
  * User Preferences Storage Manager
32
44
  *
@@ -1,6 +1,5 @@
1
1
  import { ApiResponse, OperationErrorResponse } from '../../../../bodhi-browser-ext/src/types';
2
2
  import { OpenAiApiError } from '@bodhiapp/ts-client';
3
-
4
3
  /**
5
4
  * Public API result type - discriminated union without protocol fields
6
5
  *
@@ -1,6 +1,5 @@
1
1
  import { ClientState } from './client-state';
2
2
  import { AuthState } from './user-info';
3
-
4
3
  /**
5
4
  * Discriminated union for state changes.
6
5
  * Allows single callback to handle both client state and auth state changes.
@@ -1,5 +1,4 @@
1
1
  import { OperationErrorResponse } from '../../../../bodhi-browser-ext/src/types';
2
-
3
2
  /**
4
3
  * Serialized direct client state for persistence
5
4
  * Stores minimal state needed to restore direct connection
@@ -1,5 +1,4 @@
1
1
  import { UserScope } from './user-info';
2
-
3
2
  /**
4
3
  * Log levels for client logging
5
4
  */
@@ -1,5 +1,4 @@
1
1
  import { BrowserType, OSType } from '../../../../setup-modal/src/types';
2
-
3
2
  /**
4
3
  * Browser detection result
5
4
  */
@@ -0,0 +1 @@
1
+ export declare const BUILD_MODE: string;
@@ -1,9 +1,19 @@
1
- /**
2
- * Constants for web2ext communication
3
- */
1
+ import { StorageKeys } from '../../core/src/index.ts';
4
2
  export declare const POLL_INTERVAL = 500;
5
3
  export declare const POLL_TIMEOUT = 5000;
6
4
  /**
7
5
  * LocalStorage keys for OAuth tokens and PKCE flow (namespaced with 'bodhi:web' prefix)
6
+ * @deprecated Use createWebStorageKeys(basePath) instead for basePath-aware keys
7
+ */
8
+ export declare const STORAGE_KEYS: StorageKeys;
9
+ /**
10
+ * Create storage keys for web mode with optional basePath isolation
11
+ *
12
+ * @param basePath - Base path of app (e.g., '/', '/app1/'), defaults to '/'
13
+ * @returns Storage keys with basePath-aware prefix
14
+ *
15
+ * Examples:
16
+ * - createWebStorageKeys('/') => keys with 'bodhi:web:' prefix
17
+ * - createWebStorageKeys('/app1/') => keys with '/app1/:bodhi:web:' prefix
8
18
  */
9
- export declare const STORAGE_KEYS: import('../../core/src/index.ts').StorageKeys;
19
+ export declare function createWebStorageKeys(basePath?: string): StorageKeys;
@@ -1,10 +1,10 @@
1
1
  import { DirectClientBase, AuthLoggedIn, AuthLoggedOut, DirectClientBaseConfig, StateChangeCallback } from '../../core/src/index.ts';
2
-
3
2
  /**
4
3
  * Configuration for DirectWebClient
5
4
  */
6
5
  export interface DirectWebClientConfig extends DirectClientBaseConfig {
7
6
  redirectUri: string;
7
+ basePath?: string;
8
8
  }
9
9
  /**
10
10
  * DirectWebClient - Web mode implementation using browser redirect OAuth
@@ -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
  };
@@ -23,7 +22,8 @@ export declare class WindowBodhiextClient implements IExtensionClient {
23
22
  private authEndpoints;
24
23
  private onStateChange;
25
24
  private refreshPromise;
26
- constructor(authClientId: string, config: WebClientConfig, onStateChange?: StateChangeCallback);
25
+ private storageKeys;
26
+ constructor(authClientId: string, config: WebClientConfig, onStateChange?: StateChangeCallback, basePath?: string);
27
27
  /**
28
28
  * Set client state and notify callback
29
29
  */
@@ -38,7 +38,7 @@ export declare class WindowBodhiextClient implements IExtensionClient {
38
38
  setStateCallback(callback: StateChangeCallback): void;
39
39
  /**
40
40
  * Ensure bodhiext is available, attempting to acquire it if not already set
41
- * @throws Error if client not initialized
41
+ * @throws OperationError if client not initialized
42
42
  */
43
43
  private ensureBodhiext;
44
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
  */
@@ -9,6 +8,7 @@ export interface WebClientConfig {
9
8
  authServerUrl: string;
10
9
  redirectUri: string;
11
10
  userScope: UserScope;
11
+ basePath?: string;
12
12
  logLevel: LogLevel;
13
13
  initParams?: {
14
14
  extension?: {
@@ -28,6 +28,7 @@ export declare class WebUIClient extends BaseFacadeClient<WebClientConfig, Windo
28
28
  redirectUri: string;
29
29
  authServerUrl?: string;
30
30
  userScope?: UserScope;
31
+ basePath?: string;
31
32
  logLevel?: LogLevel;
32
33
  initParams?: {
33
34
  extension?: {
@@ -1,5 +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 };
5
+ export { BUILD_MODE as WEB_BUILD_MODE } from './build-info';
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("@bodhiapp/bodhi-js-core");class S extends s.DirectClientBase{constructor(e,t){super({...e,storagePrefix:s.STORAGE_PREFIXES.DIRECT},"DirectWebClient",t),this.redirectUri=e.redirectUri}async login(){const e=await this.getAuthState();if(e.isLoggedIn)return e;const t=await this.requestResourceAccess(),r=`openid profile email roles ${this.userScope} ${t}`,o=s.generateCodeVerifier(),n=await s.generateCodeChallenge(o),i=s.generateCodeVerifier();localStorage.setItem(this.storageKeys.CODE_VERIFIER,o),localStorage.setItem(this.storageKeys.STATE,i);const c=new URL(this.authEndpoints.authorize);throw c.searchParams.set("client_id",this.authClientId),c.searchParams.set("response_type","code"),c.searchParams.set("redirect_uri",this.redirectUri),c.searchParams.set("scope",r),c.searchParams.set("code_challenge",n),c.searchParams.set("code_challenge_method","S256"),c.searchParams.set("state",i),window.location.href=c.toString(),new Error("Redirect initiated")}async handleOAuthCallback(e,t){const r=localStorage.getItem(this.storageKeys.STATE);if(!r||r!==t)throw new Error("Invalid state parameter - possible CSRF attack");await this.exchangeCodeForTokens(e),localStorage.removeItem(this.storageKeys.CODE_VERIFIER),localStorage.removeItem(this.storageKeys.STATE);const o=await this.getAuthState();if(!o.isLoggedIn)throw new Error("Login failed");const n=o;return this.setAuthState(n),n}async logout(){const e=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);if(e)try{const r=new URLSearchParams({token:e,client_id:this.authClientId,token_type_hint:"refresh_token"});await fetch(this.authEndpoints.revoke,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:r})}catch(r){this.logger.warn("Token revocation failed:",r)}localStorage.removeItem(this.storageKeys.ACCESS_TOKEN),localStorage.removeItem(this.storageKeys.REFRESH_TOKEN),localStorage.removeItem(this.storageKeys.EXPIRES_AT),localStorage.removeItem(this.storageKeys.RESOURCE_SCOPE);const t={isLoggedIn:!1};return this.setAuthState(t),t}async requestResourceAccess(){const e=await this.sendApiRequest("POST","/bodhi/v1/apps/request-access",{app_client_id:this.authClientId},{},!1);if(s.isApiResultOperationError(e))throw new Error("Failed to get resource access scope from server");if(!s.isApiResultSuccess(e))throw new Error("Failed to get resource access scope from server: API error");const t=e.body.scope;return localStorage.setItem(this.storageKeys.RESOURCE_SCOPE,t),t}async exchangeCodeForTokens(e){const t=localStorage.getItem(this.storageKeys.CODE_VERIFIER);if(!t)throw new Error("Code verifier not found");const r=await fetch(this.authEndpoints.token,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:this.redirectUri,client_id:this.authClientId,code_verifier:t})});if(!r.ok){const n=await r.text();throw new Error(`Token exchange failed: ${r.status} ${n}`)}const o=await r.json();if(localStorage.setItem(this.storageKeys.ACCESS_TOKEN,o.access_token),o.refresh_token&&localStorage.setItem(this.storageKeys.REFRESH_TOKEN,o.refresh_token),o.expires_in){const n=Date.now()+o.expires_in*1e3;localStorage.setItem(this.storageKeys.EXPIRES_AT,n.toString())}}async _storageGet(e){return localStorage.getItem(e)}async _storageSet(e){Object.entries(e).forEach(([t,r])=>{localStorage.setItem(t,String(r))})}async _storageRemove(e){e.forEach(t=>localStorage.removeItem(t))}_getRedirectUri(){return this.redirectUri}}const f=500,_=5e3,a=s.createStorageKeys(s.STORAGE_PREFIXES.WEB);class p{constructor(e,t,r){this.state=s.EXTENSION_STATE_NOT_INITIALIZED,this.bodhiext=null,this.refreshPromise=null,this.logger=new s.Logger("WindowBodhiextClient",t.logLevel),this.authClientId=e,this.config=t,this.authEndpoints=s.createOAuthEndpoints(this.config.authServerUrl),this.onStateChange=r??s.NOOP_STATE_CALLBACK}setState(e){this.state=e,this.logger.info(`{state: ${JSON.stringify(e)}} - Setting client state`),this.onStateChange({type:"client-state",state:e})}setAuthState(e){this.onStateChange({type:"auth-state",state:e})}setStateCallback(e){this.onStateChange=e}ensureBodhiext(){if(!this.bodhiext&&window.bodhiext&&(this.logger.info("Acquiring window.bodhiext reference"),this.bodhiext=window.bodhiext),!this.bodhiext)throw new Error("Client not initialized")}async sendExtRequest(e,t){return this.ensureBodhiext(),this.bodhiext.sendExtRequest(e,t)}async sendApiRequest(e,t,r,o,n){try{this.ensureBodhiext()}catch(i){return{error:{message:i instanceof Error?i.message:String(i),type:"extension_error"}}}try{let i=o||{};if(n){const l=await this._getAccessTokenRaw();if(!l)return{error:{message:"Not authenticated. Please log in first.",type:"extension_error"}};i={...i,Authorization:`Bearer ${l}`}}return await this.bodhiext.sendApiRequest(e,t,r,i)}catch(i){const c=i==null?void 0:i.error,l=(c==null?void 0:c.message)??(i instanceof Error?i.message:String(i)),h=(c==null?void 0:c.type)||"extension_error";return{error:{message:l,type:h}}}}getState(){return this.state}isClientInitialized(){return this.state.extension==="ready"}isServerReady(){return this.isClientInitialized()&&this.state.server.status==="ready"}async init(e={}){var o,n,i,c;if(!e.testConnection&&!e.selectedConnection)return this.logger.info("No testConnection or selectedConnection, returning not-initialized state"),s.EXTENSION_STATE_NOT_INITIALIZED;if(this.bodhiext&&!e.testConnection)return this.logger.debug("Already have bodhiext handle, skipping polling"),this.state;if(!this.bodhiext){const l=e.timeoutMs??((n=(o=this.config.initParams)==null?void 0:o.extension)==null?void 0:n.timeoutMs)??_,h=e.intervalMs??((c=(i=this.config.initParams)==null?void 0:i.extension)==null?void 0:c.intervalMs)??f,d=Date.now();if(!await new Promise(u=>{const E=()=>{if(window.bodhiext){this.bodhiext=window.bodhiext,u(!0);return}if(Date.now()-d>=l){u(!1);return}setTimeout(E,h)};E()}))return this.logger.warn("Extension discovery timed out"),this.setState(s.EXTENSION_STATE_NOT_FOUND),this.state}const t=await this.bodhiext.getExtensionId();this.logger.info(`Extension discovered: ${t}`);const r={type:"extension",extension:"ready",extensionId:t,server:s.PENDING_EXTENSION_READY};if(e.testConnection)try{const l=await this.getServerState();this.setState({...r,server:l}),this.logger.info(`Server connectivity tested: ${l.status}`)}catch(l){this.logger.error("Failed to get server state:",l),this.setState({...r,server:s.BACKEND_SERVER_NOT_REACHABLE})}else this.setState(r);return this.state}async requestResourceAccess(){this.ensureBodhiext();const e=await this.bodhiext.sendApiRequest("POST","/bodhi/v1/apps/request-access",{app_client_id:this.authClientId});if(!s.isApiResultSuccess(e))throw new Error("Failed to get resource access scope: API error");const t=e.body.scope;return localStorage.setItem(a.RESOURCE_SCOPE,t),t}async login(){const e=await this.getAuthState();if(e.isLoggedIn)return e;this.ensureBodhiext();const t=await this.requestResourceAccess(),r=s.generateCodeVerifier(),o=await s.generateCodeChallenge(r),n=s.generateCodeVerifier();localStorage.setItem(a.CODE_VERIFIER,r),localStorage.setItem(a.STATE,n);const i=["openid","profile","email","roles",this.config.userScope,t],c=new URLSearchParams({response_type:"code",client_id:this.authClientId,redirect_uri:this.config.redirectUri,scope:i.join(" "),state:n,code_challenge:o,code_challenge_method:"S256"}),l=`${this.authEndpoints.authorize}?${c}`;return window.location.href=l,new Promise(()=>{})}async handleOAuthCallback(e,t){const r=localStorage.getItem(a.STATE);if(!r||r!==t)throw new Error("Invalid state parameter - possible CSRF attack");await this.exchangeCodeForTokens(e),localStorage.removeItem(a.CODE_VERIFIER),localStorage.removeItem(a.STATE);const o=await this.getAuthState();if(!o.isLoggedIn)throw new Error("Login failed");return this.setAuthState(o),o}async exchangeCodeForTokens(e){const t=localStorage.getItem(a.CODE_VERIFIER);if(!t)throw new Error("Code verifier not found");const r=new URLSearchParams({grant_type:"authorization_code",client_id:this.authClientId,code:e,redirect_uri:this.config.redirectUri,code_verifier:t}),o=await fetch(this.authEndpoints.token,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:r});if(!o.ok){const i=await o.text();throw new Error(`Token exchange failed: ${o.status} ${i}`)}const n=await o.json();if(!n.access_token)throw new Error("No access token received");if(localStorage.setItem(a.ACCESS_TOKEN,n.access_token),n.refresh_token&&localStorage.setItem(a.REFRESH_TOKEN,n.refresh_token),n.expires_in){const i=Date.now()+n.expires_in*1e3;localStorage.setItem(a.EXPIRES_AT,i.toString())}}async logout(){const e=localStorage.getItem(a.REFRESH_TOKEN);if(e)try{const r=new URLSearchParams({token:e,client_id:this.authClientId,token_type_hint:"refresh_token"});await fetch(this.authEndpoints.revoke,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:r})}catch(r){this.logger.warn("Token revocation failed:",r)}localStorage.removeItem(a.ACCESS_TOKEN),localStorage.removeItem(a.REFRESH_TOKEN),localStorage.removeItem(a.EXPIRES_AT),localStorage.removeItem(a.CODE_VERIFIER),localStorage.removeItem(a.STATE),localStorage.removeItem(a.RESOURCE_SCOPE);const t={isLoggedIn:!1};return this.setAuthState(t),t}async getAuthState(){const e=await this._getAccessTokenRaw();if(!e)return{isLoggedIn:!1};try{return{isLoggedIn:!0,userInfo:s.extractUserInfo(e),accessToken:e}}catch(t){return this.logger.error("Failed to parse token:",t),{isLoggedIn:!1}}}async _getAccessTokenRaw(){const e=localStorage.getItem(a.ACCESS_TOKEN),t=localStorage.getItem(a.EXPIRES_AT);if(!e)return null;if(t){const r=parseInt(t,10);if(Date.now()>=r-5*1e3){const o=localStorage.getItem(a.REFRESH_TOKEN);return o?this._tryRefreshToken(o):null}}return e}async _tryRefreshToken(e){if(this.refreshPromise)return this.logger.debug("Refresh already in progress, returning existing promise"),this.refreshPromise;this.refreshPromise=this._doRefreshToken(e);try{return await this.refreshPromise}finally{this.refreshPromise=null}}async _doRefreshToken(e){this.logger.debug("Refreshing access token");try{const t=await s.refreshAccessToken(this.authEndpoints.token,e,this.authClientId);if(t){this._storeRefreshedTokens(t);const r=s.extractUserInfo(t.access_token);return this.setAuthState({isLoggedIn:!0,userInfo:r,accessToken:t.access_token}),this.logger.info("Token refreshed successfully"),t.access_token}}catch(t){this.logger.warn("Token refresh failed:",t)}throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"),s.createOperationError("Access token expired and unable to refresh. Try logging out and logging in again.","token_refresh_failed")}_storeRefreshedTokens(e){const t=Date.now()+e.expires_in*1e3;localStorage.setItem(a.ACCESS_TOKEN,e.access_token),localStorage.setItem(a.EXPIRES_AT,String(t)),e.refresh_token&&localStorage.setItem(a.REFRESH_TOKEN,e.refresh_token)}async pingApi(){return this.sendApiRequest("GET","/ping")}async fetchModels(){return this.sendApiRequest("GET","/v1/models",void 0,void 0,!0)}async getServerState(){const e=await this.sendApiRequest("GET","/bodhi/v1/info");if(s.isApiResultOperationError(e)||!s.isApiResultSuccess(e))return s.BACKEND_SERVER_NOT_REACHABLE;const t=e.body;switch(t.status){case"ready":return{status:"ready",version:t.version||"unknown"};case"setup":return s.backendServerNotReady("setup",t.version||"unknown");case"resource-admin":return s.backendServerNotReady("resource-admin",t.version||"unknown");case"error":return s.backendServerNotReady("error",t.version||"unknown",t.error?{message:t.error.message,type:t.error.type}:s.SERVER_ERROR_CODES.SERVER_NOT_READY);default:return s.BACKEND_SERVER_NOT_REACHABLE}}async*stream(e,t,r,o,n=!0){this.ensureBodhiext();let i=o||{};if(n){const h=await this._getAccessTokenRaw();if(!h)throw new Error("Not authenticated. Please log in first.");i={...i,Authorization:`Bearer ${h}`}}const l=this.bodhiext.sendStreamRequest(e,t,r,i).getReader();try{for(;;){const{value:h,done:d}=await l.read();if(d||h!=null&&h.done)break;yield h.body}}catch(h){if(h instanceof Error){if("response"in h){const d=h;throw s.createApiError(h.message,d.response.status,d.response.body)}throw"error"in h,s.createOperationError(h.message,"extension_error")}throw h}finally{l.releaseLock()}}async*streamChat(e,t,r=!0){yield*this.stream("POST","/v1/chat/completions",{model:e,messages:[{role:"user",content:t}],stream:!0},void 0,r)}serialize(){return{extensionId:this.state.type==="extension"&&this.state.extension==="ready"?this.state.extensionId:void 0}}async debug(){return{type:"WindowBodhiextClient",state:this.state,authState:await this.getAuthState(),bodhiextAvailable:this.bodhiext!==null,authClientId:this.authClientId,authServerUrl:this.config.authServerUrl,redirectUri:this.config.redirectUri,userScope:this.config.userScope}}}class m extends s.BaseFacadeClient{constructor(e,t,r,o){const n={redirectUri:t.redirectUri,authServerUrl:t.authServerUrl||"https://id.getbodhi.app/realms/bodhi",userScope:t.userScope||"scope_user_user",logLevel:t.logLevel||"warn",initParams:t.initParams};super(e,n,r,o)}createLogger(e){return new s.Logger("WebUIClient",e.logLevel)}createExtClient(e,t){return new p(this.authClientId,e,t)}createDirectClient(e,t,r){return new S({authClientId:e,authServerUrl:t.authServerUrl,redirectUri:t.redirectUri,userScope:t.userScope,logLevel:t.logLevel,storagePrefix:s.STORAGE_PREFIXES.WEB},r)}async handleOAuthCallback(e,t){return this.connectionMode==="direct"?this.directClient.handleOAuthCallback(e,t):this.extClient.handleOAuthCallback(e,t)}}exports.WebUIClient=m;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("@bodhiapp/bodhi-js-core");class 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=_;
@@ -1 +1,2 @@
1
1
  export * from './bodhi-js-sdk/web/src/index'
2
+ export {}
@@ -1,7 +1,8 @@
1
- import { DirectClientBase as C, STORAGE_PREFIXES as f, generateCodeVerifier as g, generateCodeChallenge as y, isApiResultOperationError as T, isApiResultSuccess as E, createStorageKeys as x, EXTENSION_STATE_NOT_INITIALIZED as _, Logger as R, createOAuthEndpoints as A, NOOP_STATE_CALLBACK as k, EXTENSION_STATE_NOT_FOUND as v, PENDING_EXTENSION_READY as O, BACKEND_SERVER_NOT_REACHABLE as d, extractUserInfo as I, refreshAccessToken as b, createOperationError as u, backendServerNotReady as S, SERVER_ERROR_CODES as P, createApiError as U, BaseFacadeClient as N } from "@bodhiapp/bodhi-js-core";
2
- class L extends C {
1
+ import { DirectClientBase as 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
- super({ ...e, storagePrefix: f.DIRECT }, "DirectWebClient", t), this.redirectUri = e.redirectUri;
4
+ const r = e.basePath || "/", s = y(r, S.DIRECT);
5
+ super({ ...e, storagePrefix: s }, "DirectWebClient", t), this.redirectUri = e.redirectUri;
5
6
  }
6
7
  // ============================================================================
7
8
  // Authentication (Browser Redirect OAuth)
@@ -10,10 +11,10 @@ class L extends C {
10
11
  const e = await this.getAuthState();
11
12
  if (e.isLoggedIn)
12
13
  return e;
13
- const t = await this.requestResourceAccess(), r = `openid profile email roles ${this.userScope} ${t}`, s = g(), i = await y(s), o = g();
14
+ const t = await this.requestResourceAccess(), r = `openid profile email roles ${this.userScope} ${t}`, s = u(), i = await _(s), o = u();
14
15
  localStorage.setItem(this.storageKeys.CODE_VERIFIER, s), localStorage.setItem(this.storageKeys.STATE, o);
15
- const a = new URL(this.authEndpoints.authorize);
16
- throw a.searchParams.set("client_id", this.authClientId), a.searchParams.set("response_type", "code"), a.searchParams.set("redirect_uri", this.redirectUri), a.searchParams.set("scope", r), a.searchParams.set("code_challenge", i), a.searchParams.set("code_challenge_method", "S256"), a.searchParams.set("state", o), window.location.href = a.toString(), new Error("Redirect initiated");
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");
17
18
  }
18
19
  async handleOAuthCallback(e, t) {
19
20
  const r = localStorage.getItem(this.storageKeys.STATE);
@@ -62,9 +63,9 @@ class L extends C {
62
63
  {},
63
64
  !1
64
65
  );
65
- if (T(e))
66
+ if (w(e))
66
67
  throw new Error("Failed to get resource access scope from server");
67
- if (!E(e))
68
+ if (!f(e))
68
69
  throw new Error("Failed to get resource access scope from server: API error");
69
70
  const t = e.body.scope;
70
71
  return localStorage.setItem(this.storageKeys.RESOURCE_SCOPE, t), t;
@@ -114,10 +115,15 @@ class L extends C {
114
115
  return this.redirectUri;
115
116
  }
116
117
  }
117
- const K = 500, F = 5e3, n = x(f.WEB);
118
+ 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
+ }
118
124
  class D {
119
- constructor(e, t, r) {
120
- this.state = _, this.bodhiext = null, this.refreshPromise = null, this.logger = new R("WindowBodhiextClient", t.logLevel), this.authClientId = e, this.config = t, this.authEndpoints = A(this.config.authServerUrl), this.onStateChange = r ?? k;
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 || "/");
121
127
  }
122
128
  /**
123
129
  * Set client state and notify callback
@@ -142,11 +148,11 @@ class D {
142
148
  // ============================================================================
143
149
  /**
144
150
  * Ensure bodhiext is available, attempting to acquire it if not already set
145
- * @throws Error if client not initialized
151
+ * @throws OperationError if client not initialized
146
152
  */
147
153
  ensureBodhiext() {
148
154
  if (!this.bodhiext && window.bodhiext && (this.logger.info("Acquiring window.bodhiext reference"), this.bodhiext = window.bodhiext), !this.bodhiext)
149
- throw new Error("Client not initialized");
155
+ throw h("Client not initialized", "extension_error");
150
156
  }
151
157
  /**
152
158
  * Send extension request via window.bodhiext.sendExtRequest
@@ -172,8 +178,8 @@ class D {
172
178
  try {
173
179
  let o = s || {};
174
180
  if (i) {
175
- const h = await this._getAccessTokenRaw();
176
- if (!h)
181
+ const c = await this._getAccessTokenRaw();
182
+ if (!c)
177
183
  return {
178
184
  error: {
179
185
  message: "Not authenticated. Please log in first.",
@@ -182,7 +188,7 @@ class D {
182
188
  };
183
189
  o = {
184
190
  ...o,
185
- Authorization: `Bearer ${h}`
191
+ Authorization: `Bearer ${c}`
186
192
  };
187
193
  }
188
194
  return await this.bodhiext.sendApiRequest(
@@ -192,11 +198,11 @@ class D {
192
198
  o
193
199
  );
194
200
  } catch (o) {
195
- const a = o == null ? void 0 : o.error, h = (a == null ? void 0 : a.message) ?? (o instanceof Error ? o.message : String(o)), c = (a == null ? void 0 : a.type) || "extension_error";
201
+ const n = o?.error, c = n?.message ?? (o instanceof Error ? o.message : String(o)), a = n?.type || "extension_error";
196
202
  return {
197
203
  error: {
198
- message: h,
199
- type: c
204
+ message: c,
205
+ type: a
200
206
  }
201
207
  };
202
208
  }
@@ -221,28 +227,27 @@ class D {
221
227
  * No extensionId storage/restoration needed - window.bodhiext handle is ephemeral
222
228
  */
223
229
  async init(e = {}) {
224
- var s, i, o, a;
225
230
  if (!e.testConnection && !e.selectedConnection)
226
- 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;
227
232
  if (this.bodhiext && !e.testConnection)
228
233
  return this.logger.debug("Already have bodhiext handle, skipping polling"), this.state;
229
234
  if (!this.bodhiext) {
230
- const h = e.timeoutMs ?? ((i = (s = this.config.initParams) == null ? void 0 : s.extension) == null ? void 0 : i.timeoutMs) ?? F, c = e.intervalMs ?? ((a = (o = this.config.initParams) == null ? void 0 : o.extension) == null ? void 0 : a.intervalMs) ?? K, l = Date.now();
231
- if (!await new Promise((p) => {
232
- const w = () => {
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 = () => {
233
238
  if (window.bodhiext) {
234
- this.bodhiext = window.bodhiext, p(!0);
239
+ this.bodhiext = window.bodhiext, c(!0);
235
240
  return;
236
241
  }
237
- if (Date.now() - l >= h) {
238
- p(!1);
242
+ if (Date.now() - o >= s) {
243
+ c(!1);
239
244
  return;
240
245
  }
241
- setTimeout(w, c);
246
+ setTimeout(a, i);
242
247
  };
243
- w();
248
+ a();
244
249
  }))
245
- return this.logger.warn("Extension discovery timed out"), this.setState(v), this.state;
250
+ return this.logger.warn("Extension discovery timed out"), this.setState(A), this.state;
246
251
  }
247
252
  const t = await this.bodhiext.getExtensionId();
248
253
  this.logger.info(`Extension discovered: ${t}`);
@@ -250,14 +255,14 @@ class D {
250
255
  type: "extension",
251
256
  extension: "ready",
252
257
  extensionId: t,
253
- server: O
258
+ server: k
254
259
  };
255
260
  if (e.testConnection)
256
261
  try {
257
- const h = await this.getServerState();
258
- this.setState({ ...r, server: h }), this.logger.info(`Server connectivity tested: ${h.status}`);
259
- } catch (h) {
260
- this.logger.error("Failed to get server state:", h), this.setState({ ...r, server: d });
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 });
261
266
  }
262
267
  else
263
268
  this.setState(r);
@@ -275,10 +280,10 @@ class D {
275
280
  const e = await this.bodhiext.sendApiRequest("POST", "/bodhi/v1/apps/request-access", {
276
281
  app_client_id: this.authClientId
277
282
  });
278
- if (!E(e))
283
+ if (!f(e))
279
284
  throw new Error("Failed to get resource access scope: API error");
280
285
  const t = e.body.scope;
281
- return localStorage.setItem(n.RESOURCE_SCOPE, t), t;
286
+ return localStorage.setItem(this.storageKeys.RESOURCE_SCOPE, t), t;
282
287
  }
283
288
  /**
284
289
  * Login via browser redirect OAuth2 + PKCE flow
@@ -289,9 +294,9 @@ class D {
289
294
  if (e.isLoggedIn)
290
295
  return e;
291
296
  this.ensureBodhiext();
292
- const t = await this.requestResourceAccess(), r = g(), s = await y(r), i = g();
293
- localStorage.setItem(n.CODE_VERIFIER, r), localStorage.setItem(n.STATE, i);
294
- const o = ["openid", "profile", "email", "roles", this.config.userScope, t], a = new URLSearchParams({
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({
295
300
  response_type: "code",
296
301
  client_id: this.authClientId,
297
302
  redirect_uri: this.config.redirectUri,
@@ -299,8 +304,8 @@ class D {
299
304
  state: i,
300
305
  code_challenge: s,
301
306
  code_challenge_method: "S256"
302
- }), h = `${this.authEndpoints.authorize}?${a}`;
303
- return window.location.href = h, new Promise(() => {
307
+ }), c = `${this.authEndpoints.authorize}?${n}`;
308
+ return window.location.href = c, new Promise(() => {
304
309
  });
305
310
  }
306
311
  /**
@@ -309,10 +314,10 @@ class D {
309
314
  * @returns AuthLoggedIn with login state and user info
310
315
  */
311
316
  async handleOAuthCallback(e, t) {
312
- const r = localStorage.getItem(n.STATE);
317
+ const r = localStorage.getItem(this.storageKeys.STATE);
313
318
  if (!r || r !== t)
314
319
  throw new Error("Invalid state parameter - possible CSRF attack");
315
- await this.exchangeCodeForTokens(e), localStorage.removeItem(n.CODE_VERIFIER), localStorage.removeItem(n.STATE);
320
+ await this.exchangeCodeForTokens(e), localStorage.removeItem(this.storageKeys.CODE_VERIFIER), localStorage.removeItem(this.storageKeys.STATE);
316
321
  const s = await this.getAuthState();
317
322
  if (!s.isLoggedIn)
318
323
  throw new Error("Login failed");
@@ -322,7 +327,7 @@ class D {
322
327
  * Exchange authorization code for tokens
323
328
  */
324
329
  async exchangeCodeForTokens(e) {
325
- const t = localStorage.getItem(n.CODE_VERIFIER);
330
+ const t = localStorage.getItem(this.storageKeys.CODE_VERIFIER);
326
331
  if (!t)
327
332
  throw new Error("Code verifier not found");
328
333
  const r = new URLSearchParams({
@@ -345,9 +350,9 @@ class D {
345
350
  const i = await s.json();
346
351
  if (!i.access_token)
347
352
  throw new Error("No access token received");
348
- if (localStorage.setItem(n.ACCESS_TOKEN, i.access_token), i.refresh_token && localStorage.setItem(n.REFRESH_TOKEN, i.refresh_token), i.expires_in) {
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) {
349
354
  const o = Date.now() + i.expires_in * 1e3;
350
- localStorage.setItem(n.EXPIRES_AT, o.toString());
355
+ localStorage.setItem(this.storageKeys.EXPIRES_AT, o.toString());
351
356
  }
352
357
  }
353
358
  /**
@@ -355,7 +360,7 @@ class D {
355
360
  * @returns AuthLoggedOut with logged out state
356
361
  */
357
362
  async logout() {
358
- const e = localStorage.getItem(n.REFRESH_TOKEN);
363
+ const e = localStorage.getItem(this.storageKeys.REFRESH_TOKEN);
359
364
  if (e)
360
365
  try {
361
366
  const r = new URLSearchParams({
@@ -373,7 +378,7 @@ class D {
373
378
  } catch (r) {
374
379
  this.logger.warn("Token revocation failed:", r);
375
380
  }
376
- localStorage.removeItem(n.ACCESS_TOKEN), localStorage.removeItem(n.REFRESH_TOKEN), localStorage.removeItem(n.EXPIRES_AT), localStorage.removeItem(n.CODE_VERIFIER), localStorage.removeItem(n.STATE), localStorage.removeItem(n.RESOURCE_SCOPE);
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);
377
382
  const t = {
378
383
  isLoggedIn: !1
379
384
  };
@@ -387,7 +392,7 @@ class D {
387
392
  if (!e)
388
393
  return { isLoggedIn: !1 };
389
394
  try {
390
- return { isLoggedIn: !0, userInfo: I(e), accessToken: e };
395
+ return { isLoggedIn: !0, userInfo: m(e), accessToken: e };
391
396
  } catch (t) {
392
397
  return this.logger.error("Failed to parse token:", t), { isLoggedIn: !1 };
393
398
  }
@@ -397,13 +402,13 @@ class D {
397
402
  * Returns null if not logged in or token expired
398
403
  */
399
404
  async _getAccessTokenRaw() {
400
- const e = localStorage.getItem(n.ACCESS_TOKEN), t = localStorage.getItem(n.EXPIRES_AT);
405
+ const e = localStorage.getItem(this.storageKeys.ACCESS_TOKEN), t = localStorage.getItem(this.storageKeys.EXPIRES_AT);
401
406
  if (!e)
402
407
  return null;
403
408
  if (t) {
404
409
  const r = parseInt(t, 10);
405
410
  if (Date.now() >= r - 5 * 1e3) {
406
- const s = localStorage.getItem(n.REFRESH_TOKEN);
411
+ const s = localStorage.getItem(this.storageKeys.REFRESH_TOKEN);
407
412
  return s ? this._tryRefreshToken(s) : null;
408
413
  }
409
414
  }
@@ -429,14 +434,14 @@ class D {
429
434
  async _doRefreshToken(e) {
430
435
  this.logger.debug("Refreshing access token");
431
436
  try {
432
- const t = await b(
437
+ const t = await v(
433
438
  this.authEndpoints.token,
434
439
  e,
435
440
  this.authClientId
436
441
  );
437
442
  if (t) {
438
443
  this._storeRefreshedTokens(t);
439
- const r = I(t.access_token);
444
+ const r = m(t.access_token);
440
445
  return this.setAuthState({
441
446
  isLoggedIn: !0,
442
447
  userInfo: r,
@@ -446,7 +451,7 @@ class D {
446
451
  } catch (t) {
447
452
  this.logger.warn("Token refresh failed:", t);
448
453
  }
449
- throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"), u(
454
+ throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"), h(
450
455
  "Access token expired and unable to refresh. Try logging out and logging in again.",
451
456
  "token_refresh_failed"
452
457
  );
@@ -456,7 +461,7 @@ class D {
456
461
  */
457
462
  _storeRefreshedTokens(e) {
458
463
  const t = Date.now() + e.expires_in * 1e3;
459
- localStorage.setItem(n.ACCESS_TOKEN, e.access_token), localStorage.setItem(n.EXPIRES_AT, String(t)), e.refresh_token && localStorage.setItem(n.REFRESH_TOKEN, e.refresh_token);
464
+ localStorage.setItem(this.storageKeys.ACCESS_TOKEN, e.access_token), localStorage.setItem(this.storageKeys.EXPIRES_AT, String(t)), e.refresh_token && localStorage.setItem(this.storageKeys.REFRESH_TOKEN, e.refresh_token);
460
465
  }
461
466
  /**
462
467
  * Ping API
@@ -482,26 +487,26 @@ class D {
482
487
  */
483
488
  async getServerState() {
484
489
  const e = await this.sendApiRequest("GET", "/bodhi/v1/info");
485
- if (T(e))
486
- return d;
487
- if (!E(e))
488
- return d;
490
+ if (w(e))
491
+ return g;
492
+ if (!f(e))
493
+ return g;
489
494
  const t = e.body;
490
495
  switch (t.status) {
491
496
  case "ready":
492
497
  return { status: "ready", version: t.version || "unknown" };
493
498
  case "setup":
494
- return S("setup", t.version || "unknown");
499
+ return E("setup", t.version || "unknown");
495
500
  case "resource-admin":
496
- return S("resource-admin", t.version || "unknown");
501
+ return E("resource-admin", t.version || "unknown");
497
502
  case "error":
498
- return S(
503
+ return E(
499
504
  "error",
500
505
  t.version || "unknown",
501
- t.error ? { message: t.error.message, type: t.error.type } : P.SERVER_NOT_READY
506
+ t.error ? { message: t.error.message, type: t.error.type } : b.SERVER_NOT_READY
502
507
  );
503
508
  default:
504
- return d;
509
+ return g;
505
510
  }
506
511
  }
507
512
  /**
@@ -512,33 +517,33 @@ class D {
512
517
  this.ensureBodhiext();
513
518
  let o = s || {};
514
519
  if (i) {
515
- const c = await this._getAccessTokenRaw();
516
- if (!c)
517
- throw new Error("Not authenticated. Please log in first.");
520
+ const a = await this._getAccessTokenRaw();
521
+ if (!a)
522
+ throw h("Not authenticated. Please log in first.", "auth_error");
518
523
  o = {
519
524
  ...o,
520
- Authorization: `Bearer ${c}`
525
+ Authorization: `Bearer ${a}`
521
526
  };
522
527
  }
523
- const h = this.bodhiext.sendStreamRequest(e, t, r, o).getReader();
528
+ const c = this.bodhiext.sendStreamRequest(e, t, r, o).getReader();
524
529
  try {
525
530
  for (; ; ) {
526
- const { value: c, done: l } = await h.read();
527
- if (l || c != null && c.done)
531
+ const { value: a, done: d } = await c.read();
532
+ if (d || a?.done)
528
533
  break;
529
- yield c.body;
534
+ yield a.body;
530
535
  }
531
- } catch (c) {
532
- if (c instanceof Error) {
533
- if ("response" in c) {
534
- const l = c;
535
- throw U(c.message, l.response.status, l.response.body);
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);
536
541
  }
537
- throw "error" in c ? u(c.message, "extension_error") : u(c.message, "extension_error");
542
+ throw "error" in a ? h(a.message, "extension_error") : h(a.message, "extension_error");
538
543
  }
539
- throw c;
544
+ throw a;
540
545
  } finally {
541
- h.releaseLock();
546
+ c.releaseLock();
542
547
  }
543
548
  }
544
549
  /**
@@ -581,32 +586,34 @@ class D {
581
586
  };
582
587
  }
583
588
  }
584
- class V extends N {
589
+ class B extends P {
585
590
  constructor(e, t, r, s) {
586
591
  const i = {
587
592
  redirectUri: t.redirectUri,
588
593
  authServerUrl: t.authServerUrl || "https://id.getbodhi.app/realms/bodhi",
589
594
  userScope: t.userScope || "scope_user_user",
595
+ basePath: t.basePath || "/",
590
596
  logLevel: t.logLevel || "warn",
591
597
  initParams: t.initParams
592
598
  };
593
- super(e, i, r, s);
599
+ super(e, i, r, s, t.basePath);
594
600
  }
595
601
  createLogger(e) {
596
- return new R("WebUIClient", e.logLevel);
602
+ return new T("WebUIClient", e.logLevel);
597
603
  }
598
604
  createExtClient(e, t) {
599
- return new D(this.authClientId, e, t);
605
+ return new D(this.authClientId, e, t, e.basePath);
600
606
  }
601
607
  createDirectClient(e, t, r) {
602
- return new L(
608
+ return new K(
603
609
  {
604
610
  authClientId: e,
605
611
  authServerUrl: t.authServerUrl,
606
612
  redirectUri: t.redirectUri,
607
613
  userScope: t.userScope,
608
614
  logLevel: t.logLevel,
609
- storagePrefix: f.WEB
615
+ storagePrefix: S.WEB,
616
+ basePath: t.basePath
610
617
  },
611
618
  r
612
619
  );
@@ -618,6 +625,8 @@ class V extends N {
618
625
  return this.connectionMode === "direct" ? this.directClient.handleOAuthCallback(e, t) : this.extClient.handleOAuthCallback(e, t);
619
626
  }
620
627
  }
628
+ const q = "production";
621
629
  export {
622
- V as WebUIClient
630
+ q as WEB_BUILD_MODE,
631
+ B as WebUIClient
623
632
  };
@@ -1,5 +1,4 @@
1
1
  import { SetupState } from './state';
2
-
3
2
  /**
4
3
  * Central registry mapping message types to their payload/response shapes
5
4
  * Single source of truth - everything else is inferred!
@@ -1,5 +1,4 @@
1
1
  import { MessageType, RequestPayload, ResponsePayload } from './message-types';
2
-
3
2
  /** Branded type for type-safe request IDs */
4
3
  export type RequestId = string & {
5
4
  readonly __brand: 'RequestId';
@@ -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",
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.3",
38
- "@bodhiapp/ts-client": "^0.1.7"
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": "^5.2.0",
52
- "vite-plugin-dts": "^3.9.1"
52
+ "vite": "^7.1.12",
53
+ "vite-plugin-dts": "^4.5.4"
53
54
  }
54
55
  }