@bodhiapp/bodhi-js 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bodhi-browser-ext/src/types/common.d.ts +1 -0
- package/dist/bodhi-js-sdk/core/src/direct-client-base.d.ts +5 -5
- package/dist/bodhi-js-sdk/core/src/facade-client-base.d.ts +5 -5
- package/dist/bodhi-js-sdk/core/src/interface.d.ts +14 -6
- package/dist/bodhi-js-sdk/core/src/onboarding/modal.d.ts +1 -1
- package/dist/bodhi-js-sdk/core/src/types/auth.d.ts +37 -0
- package/dist/bodhi-js-sdk/core/src/types/callback.d.ts +1 -1
- package/dist/bodhi-js-sdk/core/src/types/client-state.d.ts +43 -93
- package/dist/bodhi-js-sdk/core/src/types/index.d.ts +4 -3
- package/dist/bodhi-js-sdk/core/src/types/user-info.d.ts +0 -32
- package/dist/bodhi-js-sdk/web/src/direct-client.d.ts +4 -5
- package/dist/bodhi-js-sdk/web/src/ext-client.d.ts +6 -6
- package/dist/bodhi-js-sdk/web/src/facade-client.d.ts +2 -2
- package/dist/bodhi-web.cjs.js +1 -1
- package/dist/bodhi-web.esm.js +233 -223
- package/package.json +6 -3
|
@@ -26,6 +26,7 @@ export declare const ERROR_TYPES: {
|
|
|
26
26
|
readonly NETWORK_ERROR: "network_error";
|
|
27
27
|
readonly EXTENSION_ERROR: "extension_error";
|
|
28
28
|
readonly TIMEOUT_ERROR: "timeout_error";
|
|
29
|
+
readonly AUTH_ERROR: "auth_error";
|
|
29
30
|
};
|
|
30
31
|
export type ConnectionErrorType = (typeof ERROR_TYPES)[keyof typeof ERROR_TYPES];
|
|
31
32
|
export declare const DOCUMENT_STATE_COMPLETE = "complete";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { CreateChatCompletionStreamResponse } from '@bodhiapp/ts-client';
|
|
1
|
+
import { AppAccessResponse, CreateChatCompletionStreamResponse } from '@bodhiapp/ts-client';
|
|
2
2
|
import { IDirectClient } from './interface';
|
|
3
3
|
import { Logger } from './logger';
|
|
4
4
|
import { OAuthEndpoints, RefreshTokenResponse } from './oauth';
|
|
5
5
|
import { StorageKeys } from './storage';
|
|
6
|
-
import { ApiResponseResult,
|
|
6
|
+
import { ApiResponseResult, AuthState, BackendServerState, ClientState, DirectState, InitParams, LogLevel, SerializedDirectState, StateChangeCallback } from './types';
|
|
7
7
|
/**
|
|
8
8
|
* DirectClientBase - Abstract base class for DirectClient implementations
|
|
9
9
|
*
|
|
@@ -101,8 +101,8 @@ export declare abstract class DirectClientBase implements IDirectClient {
|
|
|
101
101
|
* Debug dump of DirectClient internal state
|
|
102
102
|
*/
|
|
103
103
|
debug(): Promise<Record<string, unknown>>;
|
|
104
|
-
abstract login(): Promise<
|
|
105
|
-
abstract logout(): Promise<
|
|
104
|
+
abstract login(): Promise<AuthState>;
|
|
105
|
+
abstract logout(): Promise<AuthState>;
|
|
106
106
|
getAuthState(): Promise<AuthState>;
|
|
107
107
|
protected _getAccessTokenRaw(): Promise<string | null>;
|
|
108
108
|
/**
|
|
@@ -118,7 +118,7 @@ export declare abstract class DirectClientBase implements IDirectClient {
|
|
|
118
118
|
* Store refreshed tokens
|
|
119
119
|
*/
|
|
120
120
|
protected _storeRefreshedTokens(tokens: RefreshTokenResponse): Promise<void>;
|
|
121
|
-
protected requestResourceAccess(): Promise<
|
|
121
|
+
protected requestResourceAccess(): Promise<ApiResponseResult<AppAccessResponse>>;
|
|
122
122
|
protected exchangeCodeForTokens(code: string): Promise<void>;
|
|
123
123
|
protected revokeRefreshToken(): Promise<void>;
|
|
124
124
|
protected clearAuthStorage(): Promise<void>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { CreateChatCompletionStreamResponse } from '@bodhiapp/ts-client';
|
|
2
|
-
import { IConnectionClient } from './interface';
|
|
2
|
+
import { IConnectionClient, IExtensionClient } from './interface';
|
|
3
3
|
import { Logger } from './logger';
|
|
4
4
|
import { BodhiClientUserPrefsManager } from './storage';
|
|
5
|
-
import { ApiResponseResult,
|
|
5
|
+
import { ApiResponseResult, AuthState, BackendServerState, ClientState, ConnectionMode, DirectState, ExtensionState, InitParams, SerializedClientState, SerializedDirectState, SerializedExtensionState, StateChange, StateChangeCallback } from './types';
|
|
6
6
|
/**
|
|
7
7
|
* Base facade client with common delegation logic
|
|
8
8
|
*
|
|
@@ -10,7 +10,7 @@ import { ApiResponseResult, AuthLoggedIn, AuthLoggedOut, AuthState, BackendServe
|
|
|
10
10
|
* @template TExtClient - Extension client type (implements IConnectionClient with SerializedExtensionState)
|
|
11
11
|
* @template TDirectClient - Direct client type (implements IConnectionClient with SerializedDirectState)
|
|
12
12
|
*/
|
|
13
|
-
export declare abstract class BaseFacadeClient<TConfig, TExtClient extends
|
|
13
|
+
export declare abstract class BaseFacadeClient<TConfig, TExtClient extends IExtensionClient<unknown, SerializedExtensionState>, TDirectClient extends IConnectionClient<unknown, SerializedDirectState>> {
|
|
14
14
|
protected logger: Logger;
|
|
15
15
|
protected extClient: TExtClient;
|
|
16
16
|
protected directClient: TDirectClient;
|
|
@@ -89,8 +89,8 @@ export declare abstract class BaseFacadeClient<TConfig, TExtClient extends IConn
|
|
|
89
89
|
isServerReady(): boolean;
|
|
90
90
|
sendExtRequest<TParams = void, TRes = unknown>(action: string, params?: TParams): Promise<TRes>;
|
|
91
91
|
sendApiRequest<TReq = void, TRes = unknown>(method: string, endpoint: string, body?: TReq, headers?: Record<string, string>, authenticated?: boolean): Promise<ApiResponseResult<TRes>>;
|
|
92
|
-
login(): Promise<
|
|
93
|
-
logout(): Promise<
|
|
92
|
+
login(): Promise<AuthState>;
|
|
93
|
+
logout(): Promise<AuthState>;
|
|
94
94
|
getAuthState(): Promise<AuthState>;
|
|
95
95
|
pingApi(): Promise<ApiResponseResult<{
|
|
96
96
|
message: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CreateChatCompletionStreamResponse } from '@bodhiapp/ts-client';
|
|
2
|
-
import { ApiResponseResult,
|
|
2
|
+
import { ApiResponseResult, AuthState, BackendServerState, ClientState, ConnectionMode, DirectState, ExtensionState, InitParams, StateChangeCallback } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* ConnectionClient - Base interface for all client implementations
|
|
5
5
|
*
|
|
@@ -85,14 +85,14 @@ export interface IConnectionClient<IParams = unknown, SerialState = unknown> {
|
|
|
85
85
|
* Login via OAuth
|
|
86
86
|
* - IExtensionClient: Delegates to extension (chrome.identity or browser redirect)
|
|
87
87
|
* - DirectClient: Direct HTTP OAuth flow
|
|
88
|
-
* @returns
|
|
88
|
+
* @returns AuthState with login state and user info
|
|
89
89
|
*/
|
|
90
|
-
login(): Promise<
|
|
90
|
+
login(): Promise<AuthState>;
|
|
91
91
|
/**
|
|
92
92
|
* Logout and revoke tokens
|
|
93
|
-
* @returns
|
|
93
|
+
* @returns AuthState with logged out state
|
|
94
94
|
*/
|
|
95
|
-
logout(): Promise<
|
|
95
|
+
logout(): Promise<AuthState>;
|
|
96
96
|
/**
|
|
97
97
|
* Get current authentication state
|
|
98
98
|
* @returns AuthState (discriminated union: AuthLoggedIn | AuthLoggedOut)
|
|
@@ -205,6 +205,14 @@ export type UIClient = IConnectionClient<InitParams> & {
|
|
|
205
205
|
* @returns DirectState
|
|
206
206
|
*/
|
|
207
207
|
getDirectState(): Promise<DirectState>;
|
|
208
|
+
/**
|
|
209
|
+
* Send extension request (only available in extension mode)
|
|
210
|
+
* @param action - Extension action
|
|
211
|
+
* @param params - Action parameters
|
|
212
|
+
* @returns Response from extension
|
|
213
|
+
* @throws Error if connection mode is not 'extension'
|
|
214
|
+
*/
|
|
215
|
+
sendExtRequest<TParams = void, TRes = unknown>(action: string, params?: TParams): Promise<TRes>;
|
|
208
216
|
};
|
|
209
217
|
/**
|
|
210
218
|
* Web-specific UIClient interface with OAuth callback handling
|
|
@@ -217,7 +225,7 @@ export interface IWebUIClient extends UIClient {
|
|
|
217
225
|
* @param state - State parameter for CSRF protection
|
|
218
226
|
* @returns AuthLoggedIn with login state and user info
|
|
219
227
|
*/
|
|
220
|
-
handleOAuthCallback(code: string, state: string): Promise<
|
|
228
|
+
handleOAuthCallback(code: string, state: string): Promise<AuthState>;
|
|
221
229
|
}
|
|
222
230
|
/**
|
|
223
231
|
* Type guard to check if client has OAuth callback handling (web mode)
|
|
@@ -31,7 +31,7 @@ export interface OnboardingModalConfig {
|
|
|
31
31
|
/**
|
|
32
32
|
* Path to modal HTML file relative to extension root
|
|
33
33
|
* Used with chrome.runtime.getURL() in extension context
|
|
34
|
-
* @default 'src/
|
|
34
|
+
* @default 'src/bodhi-js-core/setup-modal.html'
|
|
35
35
|
*/
|
|
36
36
|
modalHtmlPath?: string;
|
|
37
37
|
/**
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { UserInfo } from './user-info';
|
|
2
|
+
/**
|
|
3
|
+
* Authentication status enum
|
|
4
|
+
*/
|
|
5
|
+
export type AuthStatus = 'idle' | 'loading' | 'authenticated' | 'unauthenticated' | 'error';
|
|
6
|
+
/**
|
|
7
|
+
* Authentication error
|
|
8
|
+
*/
|
|
9
|
+
export interface AuthError {
|
|
10
|
+
code: string;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Authentication state (flat interface)
|
|
15
|
+
*/
|
|
16
|
+
export interface AuthState {
|
|
17
|
+
status: AuthStatus;
|
|
18
|
+
user: UserInfo | null;
|
|
19
|
+
accessToken: string | null;
|
|
20
|
+
error: AuthError | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Helper to check if authenticated
|
|
24
|
+
*/
|
|
25
|
+
export declare function isAuthenticated(state: AuthState): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Helper to check if loading
|
|
28
|
+
*/
|
|
29
|
+
export declare function isAuthLoading(state: AuthState): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Helper to check if error
|
|
32
|
+
*/
|
|
33
|
+
export declare function isAuthError(state: AuthState): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Initial auth state
|
|
36
|
+
*/
|
|
37
|
+
export declare const INITIAL_AUTH_STATE: AuthState;
|
|
@@ -27,51 +27,24 @@ export declare const SERVER_ERROR_CODES: {
|
|
|
27
27
|
readonly type: "extension_error";
|
|
28
28
|
};
|
|
29
29
|
};
|
|
30
|
-
export declare const BACKEND_SERVER_NOT_REACHABLE: BackendServerNotReachableState;
|
|
31
30
|
/**
|
|
32
|
-
*
|
|
31
|
+
* All possible server status values
|
|
32
|
+
* Unified across extension and direct modes
|
|
33
33
|
*/
|
|
34
|
-
export
|
|
35
|
-
status: 'ready';
|
|
36
|
-
version: string;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Server is reachable but needs configuration - has error details
|
|
40
|
-
* error code: 'server-not-ready'
|
|
41
|
-
*/
|
|
42
|
-
export interface BackendServerNotReadyState {
|
|
43
|
-
status: 'setup' | 'resource-admin' | 'error';
|
|
44
|
-
version: string;
|
|
45
|
-
error: OperationErrorResponse;
|
|
46
|
-
}
|
|
34
|
+
export type ServerStatus = 'not-connected' | 'pending-extension-ready' | 'ready' | 'setup' | 'resource-admin' | 'error' | 'not-reachable';
|
|
47
35
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*/
|
|
51
|
-
export interface BackendServerNotReachableState {
|
|
52
|
-
status: 'not-reachable';
|
|
53
|
-
error: OperationErrorResponse;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Backend server state from /bodhi/v1/info endpoint
|
|
57
|
-
* Discriminated union with definite fields per state
|
|
36
|
+
* Backend server state - flat interface with nullable fields
|
|
37
|
+
* Replaces discriminated union of 5 separate interfaces
|
|
58
38
|
* Different from setup-modal's ServerState which includes UI states
|
|
59
39
|
*/
|
|
60
|
-
export
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
export interface PendingExtensionReadyState {
|
|
65
|
-
status: 'pending-extension-ready';
|
|
66
|
-
}
|
|
67
|
-
export declare const PENDING_EXTENSION_READY: PendingExtensionReadyState;
|
|
68
|
-
/**
|
|
69
|
-
* State indicating direct connection not yet established
|
|
70
|
-
*/
|
|
71
|
-
export interface BackendServerNotConnectedState {
|
|
72
|
-
status: 'not-connected';
|
|
40
|
+
export interface BackendServerState {
|
|
41
|
+
status: ServerStatus;
|
|
42
|
+
version: string | null;
|
|
43
|
+
error: OperationErrorResponse | null;
|
|
73
44
|
}
|
|
74
|
-
export declare const
|
|
45
|
+
export declare const BACKEND_SERVER_NOT_REACHABLE: BackendServerState;
|
|
46
|
+
export declare const PENDING_EXTENSION_READY: BackendServerState;
|
|
47
|
+
export declare const BACKEND_SERVER_NOT_CONNECTED: BackendServerState;
|
|
75
48
|
/**
|
|
76
49
|
* Raw response from /bodhi/v1/info endpoint
|
|
77
50
|
*/
|
|
@@ -80,8 +53,8 @@ export interface ServerInfoResponse {
|
|
|
80
53
|
version?: string;
|
|
81
54
|
error?: OperationErrorResponse;
|
|
82
55
|
}
|
|
83
|
-
export declare function isServerReady(state: BackendServerState):
|
|
84
|
-
export declare function backendServerNotReady(status: 'setup' | 'resource-admin' | 'error', version?: string, error?: OperationErrorResponse):
|
|
56
|
+
export declare function isServerReady(state: BackendServerState): boolean;
|
|
57
|
+
export declare function backendServerNotReady(status: 'setup' | 'resource-admin' | 'error', version?: string, error?: OperationErrorResponse): BackendServerState;
|
|
85
58
|
/**
|
|
86
59
|
* ClientState - Unified state for extension or direct connectivity
|
|
87
60
|
* Discriminated union with type field: 'extension' | 'direct'
|
|
@@ -115,60 +88,37 @@ export interface InitParams {
|
|
|
115
88
|
}
|
|
116
89
|
export declare function isExtensionState(state: ClientState): state is ExtensionState;
|
|
117
90
|
export declare function isDirectState(state: ClientState): state is DirectState;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
export interface
|
|
123
|
-
type: 'direct';
|
|
124
|
-
url: string;
|
|
125
|
-
server: BackendServerNotConnectedState | BackendServerState;
|
|
126
|
-
}
|
|
127
|
-
export interface DirectStateReady {
|
|
128
|
-
type: 'direct';
|
|
129
|
-
url: string;
|
|
130
|
-
server: BackendServerReadyState;
|
|
131
|
-
}
|
|
132
|
-
export interface DirectStateNotReachable {
|
|
91
|
+
/**
|
|
92
|
+
* DirectState - Flat interface with nullable url
|
|
93
|
+
* url is null when not initialized, string when configured
|
|
94
|
+
*/
|
|
95
|
+
export interface DirectState {
|
|
133
96
|
type: 'direct';
|
|
134
|
-
url: string;
|
|
135
|
-
server:
|
|
97
|
+
url: string | null;
|
|
98
|
+
server: BackendServerState;
|
|
136
99
|
}
|
|
137
|
-
export
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
100
|
+
export declare function isDirectServerReady(state: DirectState): boolean;
|
|
101
|
+
export declare function isDirectClientReady(state: DirectState): boolean;
|
|
102
|
+
export declare const DIRECT_STATE_NOT_INITIALIZED: DirectState;
|
|
103
|
+
export declare function createDirectStateReady(url: string, version?: string): DirectState;
|
|
104
|
+
export declare function createDirectStateNotReachable(url: string): DirectState;
|
|
105
|
+
export declare function createDirectStateNotReady(url: string, server: BackendServerState): DirectState;
|
|
106
|
+
/**
|
|
107
|
+
* ExtensionState - Flat interface with nullable extensionId
|
|
108
|
+
* extensionId is null when not ready, string when ready
|
|
109
|
+
*/
|
|
110
|
+
export interface ExtensionState {
|
|
111
|
+
type: 'extension';
|
|
112
|
+
extension: 'not-initialized' | 'not-found' | 'ready';
|
|
113
|
+
extensionId: string | null;
|
|
114
|
+
server: BackendServerState;
|
|
141
115
|
}
|
|
142
|
-
export type DirectState = DirectStateNotInitialized | DirectStateInitialized | DirectStateReady | DirectStateNotReachable | DirectStateNotReady;
|
|
143
|
-
export declare function isDirectServerReady(state: DirectState): state is DirectStateReady;
|
|
144
|
-
export declare function isDirectClientReady(state: DirectState): state is DirectStateInitialized | DirectStateReady | DirectStateNotReachable | DirectStateNotReady;
|
|
145
|
-
export declare const DIRECT_STATE_NOT_INITIALIZED: DirectStateNotInitialized;
|
|
146
|
-
export declare function createDirectStateReady(url: string, version?: string): DirectStateReady;
|
|
147
|
-
export declare function createDirectStateNotReachable(url: string): DirectStateNotReachable;
|
|
148
|
-
export declare function createDirectStateNotReady(url: string, server: BackendServerNotReadyState): DirectStateNotReady;
|
|
149
116
|
export declare const EXTENSION_STATE_NOT_INITIALIZED: ExtensionState;
|
|
150
117
|
export declare const EXTENSION_STATE_NOT_FOUND: ExtensionState;
|
|
151
|
-
export
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
export interface ExtensionStateNotFound {
|
|
157
|
-
type: 'extension';
|
|
158
|
-
extension: 'not-found';
|
|
159
|
-
server: PendingExtensionReadyState;
|
|
160
|
-
}
|
|
161
|
-
export interface ExtensionStateReady {
|
|
162
|
-
type: 'extension';
|
|
163
|
-
extension: 'ready';
|
|
164
|
-
extensionId: string;
|
|
165
|
-
server: PendingExtensionReadyState | BackendServerState;
|
|
166
|
-
}
|
|
167
|
-
export type ExtensionState = ExtensionStateNotInitialized | ExtensionStateNotFound | ExtensionStateReady;
|
|
168
|
-
export declare function isExtensionServerReady(state: ExtensionState): state is ExtensionStateReady;
|
|
169
|
-
export declare function isExtensionClientReady(state: ExtensionState): state is ExtensionStateReady;
|
|
170
|
-
export declare function createExtensionStateNotInitialized(): ExtensionStateNotInitialized;
|
|
171
|
-
export declare function createExtensionStateNotFound(): ExtensionStateNotFound;
|
|
118
|
+
export declare function isExtensionServerReady(state: ExtensionState): boolean;
|
|
119
|
+
export declare function isExtensionClientReady(state: ExtensionState): boolean;
|
|
120
|
+
export declare function createExtensionStateNotInitialized(): ExtensionState;
|
|
121
|
+
export declare function createExtensionStateNotFound(): ExtensionState;
|
|
172
122
|
/**
|
|
173
123
|
* Check if client is ready (has handle/url) - does NOT require server ready
|
|
174
124
|
* Use this for auth checks and reload state checks
|
|
@@ -177,14 +127,14 @@ export declare function isClientReady(state: ClientState): boolean;
|
|
|
177
127
|
/**
|
|
178
128
|
* Get backend server state from client state
|
|
179
129
|
*/
|
|
180
|
-
export declare function getBackendServerState(state: ClientState): BackendServerState
|
|
130
|
+
export declare function getBackendServerState(state: ClientState): BackendServerState;
|
|
181
131
|
/**
|
|
182
132
|
* Safely get extension ID from client state
|
|
183
|
-
* @returns Extension ID if
|
|
133
|
+
* @returns Extension ID if present, undefined otherwise
|
|
184
134
|
*/
|
|
185
135
|
export declare function getExtensionId(state: ClientState): string | undefined;
|
|
186
136
|
/**
|
|
187
137
|
* Safely get server URL from client state
|
|
188
|
-
* @returns Server URL if
|
|
138
|
+
* @returns Server URL if present, undefined otherwise
|
|
189
139
|
*/
|
|
190
140
|
export declare function getServerUrl(state: ClientState): string | undefined;
|
|
@@ -5,9 +5,10 @@ export { isApiResultError, isApiResultOperationError, isApiResultSuccess } from
|
|
|
5
5
|
export type { ApiResponseResult } from './api';
|
|
6
6
|
export { createApiError, createOperationError } from '../errors';
|
|
7
7
|
export { BACKEND_SERVER_NOT_CONNECTED, BACKEND_SERVER_NOT_REACHABLE, backendServerNotReady, createDirectStateNotReachable, createDirectStateNotReady, createDirectStateReady, createExtensionStateNotFound, createExtensionStateNotInitialized, DIRECT_STATE_NOT_INITIALIZED, EXTENSION_STATE_NOT_FOUND, EXTENSION_STATE_NOT_INITIALIZED, getBackendServerState, getExtensionId, getServerUrl, isClientReady, isDirectClientReady, isDirectServerReady, isDirectState, isExtensionClientReady, isExtensionServerReady, isExtensionState, isServerReady, PENDING_EXTENSION_READY, SERVER_ERROR_CODES, } from './client-state';
|
|
8
|
-
export type {
|
|
9
|
-
export {
|
|
10
|
-
export type {
|
|
8
|
+
export type { BackendServerState, ClientState, ConnectionMode, DirectState, ExtensionState, InitParams, SerializedClientState, SerializedDirectState, SerializedExtensionState, ServerInfoResponse, ServerStatus, } from './client-state';
|
|
9
|
+
export { INITIAL_AUTH_STATE, isAuthError, isAuthLoading, isAuthenticated } from './auth';
|
|
10
|
+
export type { AuthError, AuthState, AuthStatus } from './auth';
|
|
11
|
+
export type { Tokens, UserInfo, UserScope } from './user-info';
|
|
11
12
|
export type { ClientConfig, DiscoveryResult, LogLevel } from './config';
|
|
12
13
|
export type { BrowserInfo, OSInfo } from './platform';
|
|
13
14
|
export { NOOP_STATE_CALLBACK } from './callback';
|
|
@@ -25,35 +25,3 @@ export interface Tokens {
|
|
|
25
25
|
idToken?: string;
|
|
26
26
|
expiresIn: number;
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Authentication state when user is logged in
|
|
30
|
-
*/
|
|
31
|
-
export interface AuthLoggedIn {
|
|
32
|
-
isLoggedIn: true;
|
|
33
|
-
userInfo: UserInfo;
|
|
34
|
-
accessToken: string;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Authentication state when user is logged out
|
|
38
|
-
*/
|
|
39
|
-
export interface AuthLoggedOut {
|
|
40
|
-
isLoggedIn: false;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Authentication state when there is an error
|
|
44
|
-
*/
|
|
45
|
-
export interface AuthError {
|
|
46
|
-
isLoggedIn: false;
|
|
47
|
-
error: {
|
|
48
|
-
message: string;
|
|
49
|
-
code: string;
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
export declare const AUTH_CLIENT_NOT_INITIALIZED: AuthError;
|
|
53
|
-
export declare function isAuthError(state: unknown): state is AuthError;
|
|
54
|
-
export declare function isAuthLoggedOut(state: unknown): state is AuthLoggedOut;
|
|
55
|
-
export declare function isAuthLoggedIn(state: unknown): state is AuthLoggedIn;
|
|
56
|
-
/**
|
|
57
|
-
* Authentication state (discriminated union)
|
|
58
|
-
*/
|
|
59
|
-
export type AuthState = AuthLoggedIn | AuthLoggedOut | AuthError;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DirectClientBase,
|
|
1
|
+
import { DirectClientBase, AuthState, DirectClientBaseConfig, StateChangeCallback } from '../../core/src/index.ts';
|
|
2
2
|
/**
|
|
3
3
|
* Configuration for DirectWebClient
|
|
4
4
|
*/
|
|
@@ -12,10 +12,9 @@ export interface DirectWebClientConfig extends DirectClientBaseConfig {
|
|
|
12
12
|
export declare class DirectWebClient extends DirectClientBase {
|
|
13
13
|
private redirectUri;
|
|
14
14
|
constructor(config: DirectWebClientConfig, onStateChange?: StateChangeCallback);
|
|
15
|
-
login(): Promise<
|
|
16
|
-
handleOAuthCallback(code: string, state: string): Promise<
|
|
17
|
-
logout(): Promise<
|
|
18
|
-
protected requestResourceAccess(): Promise<string>;
|
|
15
|
+
login(): Promise<AuthState>;
|
|
16
|
+
handleOAuthCallback(code: string, state: string): Promise<AuthState>;
|
|
17
|
+
logout(): Promise<AuthState>;
|
|
19
18
|
protected exchangeCodeForTokens(code: string): Promise<void>;
|
|
20
19
|
protected _storageGet(key: string): Promise<string | null>;
|
|
21
20
|
protected _storageSet(items: Record<string, string | number>): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApiResponseResult,
|
|
1
|
+
import { ApiResponseResult, 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
4
|
export type SerializedWebExtensionState = {
|
|
@@ -71,15 +71,15 @@ export declare class WindowBodhiextClient implements IExtensionClient {
|
|
|
71
71
|
private requestResourceAccess;
|
|
72
72
|
/**
|
|
73
73
|
* Login via browser redirect OAuth2 + PKCE flow
|
|
74
|
-
* @returns
|
|
74
|
+
* @returns AuthState (though in practice, this redirects and never returns)
|
|
75
75
|
*/
|
|
76
|
-
login(): Promise<
|
|
76
|
+
login(): Promise<AuthState>;
|
|
77
77
|
/**
|
|
78
78
|
* Handle OAuth callback with authorization code
|
|
79
79
|
* Should be called from callback page with extracted URL params
|
|
80
|
-
* @returns
|
|
80
|
+
* @returns AuthState with login state and user info
|
|
81
81
|
*/
|
|
82
|
-
handleOAuthCallback(code: string, state: string): Promise<
|
|
82
|
+
handleOAuthCallback(code: string, state: string): Promise<AuthState>;
|
|
83
83
|
/**
|
|
84
84
|
* Exchange authorization code for tokens
|
|
85
85
|
*/
|
|
@@ -88,7 +88,7 @@ export declare class WindowBodhiextClient implements IExtensionClient {
|
|
|
88
88
|
* Logout user and revoke tokens
|
|
89
89
|
* @returns AuthLoggedOut with logged out state
|
|
90
90
|
*/
|
|
91
|
-
logout(): Promise<
|
|
91
|
+
logout(): Promise<AuthState>;
|
|
92
92
|
/**
|
|
93
93
|
* Get current authentication state
|
|
94
94
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseFacadeClient, Logger,
|
|
1
|
+
import { BaseFacadeClient, Logger, AuthState, 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
4
|
/**
|
|
@@ -40,5 +40,5 @@ export declare class WebUIClient extends BaseFacadeClient<WebClientConfig, Windo
|
|
|
40
40
|
protected createLogger(config: WebClientConfig): Logger;
|
|
41
41
|
protected createExtClient(config: WebClientConfig, onStateChange: (change: StateChange) => void): WindowBodhiextClient;
|
|
42
42
|
protected createDirectClient(authClientId: string, config: WebClientConfig, onStateChange: (change: StateChange) => void): DirectWebClient;
|
|
43
|
-
handleOAuthCallback(code: string, state: string): Promise<
|
|
43
|
+
handleOAuthCallback(code: string, state: string): Promise<AuthState>;
|
|
44
44
|
}
|
package/dist/bodhi-web.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("@bodhiapp/bodhi-js-core");class 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
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("@bodhiapp/bodhi-js-core");class d extends r.DirectClientBase{constructor(t,e){const s=t.basePath||"/",o=r.createStoragePrefixWithBasePath(s,r.STORAGE_PREFIXES.DIRECT);super({...t,storagePrefix:o},"DirectWebClient",e),this.redirectUri=t.redirectUri}async login(){const t=await this.getAuthState();if(t.status==="authenticated")return t;const e=await this.requestResourceAccess();if(r.isApiResultOperationError(e))throw r.createOperationError(e.error.message,e.error.type);if(r.isApiResultError(e)){const{message:n}=e.body.error;throw r.createOperationError(n,"auth_error")}if(!r.isApiResultSuccess(e))throw r.createOperationError(`Unexpected HTTP ${e.status}`,"auth_error");const s=e.body.scope;localStorage.setItem(this.storageKeys.RESOURCE_SCOPE,s);const o=`openid profile email roles ${this.userScope} ${s}`,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",o),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(t,e){const s=localStorage.getItem(this.storageKeys.STATE);if(!s||s!==e)throw new Error("Invalid state parameter - possible CSRF attack");await this.exchangeCodeForTokens(t),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 t=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);if(t)try{const s=new URLSearchParams({token:t,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:s})}catch(s){this.logger.warn("Token revocation failed:",s)}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 e={status:"unauthenticated",user:null,accessToken:null,error:null};return this.setAuthState(e),e}async exchangeCodeForTokens(t){const e=localStorage.getItem(this.storageKeys.CODE_VERIFIER);if(!e)throw new Error("Code verifier not found");const s=await fetch(this.authEndpoints.token,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:t,redirect_uri:this.redirectUri,client_id:this.authClientId,code_verifier:e})});if(!s.ok){const i=await s.text();throw new Error(`Token exchange failed: ${s.status} ${i}`)}const o=await s.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 i=Date.now()+o.expires_in*1e3;localStorage.setItem(this.storageKeys.EXPIRES_AT,i.toString())}}async _storageGet(t){return localStorage.getItem(t)}async _storageSet(t){Object.entries(t).forEach(([e,s])=>{localStorage.setItem(e,String(s))})}async _storageRemove(t){t.forEach(e=>localStorage.removeItem(e))}_getRedirectUri(){return this.redirectUri}}const g=500,E=5e3;r.createStorageKeys(r.STORAGE_PREFIXES.WEB);function S(u="/"){const t=r.createStoragePrefixWithBasePath(u,r.STORAGE_PREFIXES.WEB);return r.createStorageKeys(t)}class f{constructor(t,e,s,o){this.state=r.EXTENSION_STATE_NOT_INITIALIZED,this.bodhiext=null,this.refreshPromise=null,this.logger=new r.Logger("WindowBodhiextClient",e.logLevel),this.authClientId=t,this.config=e,this.authEndpoints=r.createOAuthEndpoints(this.config.authServerUrl),this.onStateChange=s??r.NOOP_STATE_CALLBACK,this.storageKeys=S(o||"/")}setState(t){this.state=t,this.logger.info(`{state: ${JSON.stringify(t)}} - Setting client state`),this.onStateChange({type:"client-state",state:t})}setAuthState(t){this.onStateChange({type:"auth-state",state:t})}setStateCallback(t){this.onStateChange=t}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(t,e){return this.ensureBodhiext(),this.bodhiext.sendExtRequest(t,e)}async sendApiRequest(t,e,s,o,i){try{this.ensureBodhiext()}catch(a){return{error:{message:a instanceof Error?a.message:String(a),type:"extension_error"}}}try{let a=o||{};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(t,e,s,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(t={}){if(!t.testConnection&&!t.selectedConnection)return this.logger.info("No testConnection or selectedConnection, returning not-initialized state"),r.EXTENSION_STATE_NOT_INITIALIZED;if(this.bodhiext&&!t.testConnection)return this.logger.debug("Already have bodhiext handle, skipping polling"),this.state;if(!this.bodhiext){const o=t.timeoutMs??this.config.initParams?.extension?.timeoutMs??E,i=t.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>=o){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 e=await this.bodhiext.getExtensionId();this.logger.info(`Extension discovered: ${e}`);const s={type:"extension",extension:"ready",extensionId:e,server:r.PENDING_EXTENSION_READY};if(t.testConnection)try{const o=await this.getServerState();this.setState({...s,server:o}),this.logger.info(`Server connectivity tested: ${o.status}`)}catch(o){this.logger.error("Failed to get server state:",o),this.setState({...s,server:r.BACKEND_SERVER_NOT_REACHABLE})}else this.setState(s);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 t=await this.getAuthState();if(t.status==="authenticated")return t;this.ensureBodhiext();const e=await this.requestResourceAccess();if(r.isApiResultOperationError(e))throw r.createOperationError(e.error.message,e.error.type);if(r.isApiResultError(e)){const{message:l}=e.body.error;throw r.createOperationError(l,"auth_error")}if(!r.isApiResultSuccess(e))throw r.createOperationError(`Unexpected HTTP ${e.status}`,"auth_error");const s=e.body.scope;localStorage.setItem(this.storageKeys.RESOURCE_SCOPE,s);const o=r.generateCodeVerifier(),i=await r.generateCodeChallenge(o),a=r.generateCodeVerifier();localStorage.setItem(this.storageKeys.CODE_VERIFIER,o),localStorage.setItem(this.storageKeys.STATE,a);const h=["openid","profile","email","roles",this.config.userScope,s],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(t,e){const s=localStorage.getItem(this.storageKeys.STATE);if(!s||s!==e)throw new Error("Invalid state parameter - possible CSRF attack");await this.exchangeCodeForTokens(t),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(t){const e=localStorage.getItem(this.storageKeys.CODE_VERIFIER);if(!e)throw new Error("Code verifier not found");const s=new URLSearchParams({grant_type:"authorization_code",client_id:this.authClientId,code:t,redirect_uri:this.config.redirectUri,code_verifier:e}),o=await fetch(this.authEndpoints.token,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:s});if(!o.ok){const a=await o.text();throw new Error(`Token exchange failed: ${o.status} ${a}`)}const i=await o.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 t=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);if(t)try{const s=new URLSearchParams({token:t,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:s})}catch(s){this.logger.warn("Token revocation failed:",s)}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 e={status:"unauthenticated",user:null,accessToken:null,error:null};return this.setAuthState(e),e}async getAuthState(){const t=await this._getAccessTokenRaw();if(!t)return{status:"unauthenticated",user:null,accessToken:null,error:null};try{return{status:"authenticated",user:r.extractUserInfo(t),accessToken:t,error:null}}catch(e){return this.logger.error("Failed to parse token:",e),{status:"unauthenticated",user:null,accessToken:null,error:null}}}async _getAccessTokenRaw(){const t=localStorage.getItem(this.storageKeys.ACCESS_TOKEN),e=localStorage.getItem(this.storageKeys.EXPIRES_AT);if(!t)return null;if(e){const s=parseInt(e,10);if(Date.now()>=s-5*1e3){const o=localStorage.getItem(this.storageKeys.REFRESH_TOKEN);return o?this._tryRefreshToken(o):null}}return t}async _tryRefreshToken(t){if(this.refreshPromise)return this.logger.debug("Refresh already in progress, returning existing promise"),this.refreshPromise;this.refreshPromise=this._doRefreshToken(t);try{return await this.refreshPromise}finally{this.refreshPromise=null}}async _doRefreshToken(t){this.logger.debug("Refreshing access token");try{const e=await r.refreshAccessToken(this.authEndpoints.token,t,this.authClientId);if(e){this._storeRefreshedTokens(e);const s=r.extractUserInfo(e.access_token);return this.setAuthState({status:"authenticated",user:s,accessToken:e.access_token,error:null}),this.logger.info("Token refreshed successfully"),e.access_token}}catch(e){this.logger.warn("Token refresh failed:",e)}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")}_storeRefreshedTokens(t){const e=Date.now()+t.expires_in*1e3;localStorage.setItem(this.storageKeys.ACCESS_TOKEN,t.access_token),localStorage.setItem(this.storageKeys.EXPIRES_AT,String(e)),t.refresh_token&&localStorage.setItem(this.storageKeys.REFRESH_TOKEN,t.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 t=await this.sendApiRequest("GET","/bodhi/v1/info");if(r.isApiResultOperationError(t)||!r.isApiResultSuccess(t))return r.BACKEND_SERVER_NOT_REACHABLE;const e=t.body;switch(e.status){case"ready":return{status:"ready",version:e.version||"unknown",error:null};case"setup":return r.backendServerNotReady("setup",e.version||"unknown");case"resource-admin":return r.backendServerNotReady("resource-admin",e.version||"unknown");case"error":return r.backendServerNotReady("error",e.version||"unknown",e.error?{message:e.error.message,type:e.error.type}:r.SERVER_ERROR_CODES.SERVER_NOT_READY);default:return r.BACKEND_SERVER_NOT_REACHABLE}}async*stream(t,e,s,o,i=!0){this.ensureBodhiext();let a=o||{};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(t,e,s,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()}}async*streamChat(t,e,s=!0){yield*this.stream("POST","/v1/chat/completions",{model:t,messages:[{role:"user",content:e}],stream:!0},void 0,s)}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 r.BaseFacadeClient{constructor(t,e,s,o){const i={redirectUri:e.redirectUri,authServerUrl:e.authServerUrl||"https://id.getbodhi.app/realms/bodhi",userScope:e.userScope||"scope_user_user",basePath:e.basePath||"/",logLevel:e.logLevel||"warn",initParams:e.initParams};super(t,i,s,o,e.basePath)}createLogger(t){return new r.Logger("WebUIClient",t.logLevel)}createExtClient(t,e){return new f(this.authClientId,t,e,t.basePath)}createDirectClient(t,e,s){return new d({authClientId:t,authServerUrl:e.authServerUrl,redirectUri:e.redirectUri,userScope:e.userScope,logLevel:e.logLevel,storagePrefix:r.STORAGE_PREFIXES.WEB,basePath:e.basePath},s)}async handleOAuthCallback(t,e){return this.connectionMode==="direct"?this.directClient.handleOAuthCallback(t,e):this.extClient.handleOAuthCallback(t,e)}}const p="production";exports.WEB_BUILD_MODE=p;exports.WebUIClient=_;
|