@bodhiapp/bodhi-js 0.0.2 → 0.0.3
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-web.cjs.js +1 -749
- package/dist/bodhi-web.esm.js +267 -393
- package/package.json +2 -2
- package/dist/bodhi-web.cjs.js.map +0 -1
- package/dist/bodhi-web.esm.js.map +0 -1
package/dist/bodhi-web.cjs.js
CHANGED
|
@@ -1,749 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const bodhiJsCore = require("@bodhiapp/bodhi-js-core");
|
|
4
|
-
class DirectWebClient extends bodhiJsCore.DirectClientBase {
|
|
5
|
-
constructor(config, onStateChange) {
|
|
6
|
-
super({ ...config, storagePrefix: bodhiJsCore.STORAGE_PREFIXES.DIRECT }, "DirectWebClient", onStateChange);
|
|
7
|
-
this.redirectUri = config.redirectUri;
|
|
8
|
-
}
|
|
9
|
-
// ============================================================================
|
|
10
|
-
// Authentication (Browser Redirect OAuth)
|
|
11
|
-
// ============================================================================
|
|
12
|
-
async login() {
|
|
13
|
-
const existingAuth = await this.getAuthState();
|
|
14
|
-
if (existingAuth.isLoggedIn) {
|
|
15
|
-
return existingAuth;
|
|
16
|
-
}
|
|
17
|
-
const resourceScope = await this.requestResourceAccess();
|
|
18
|
-
const fullScope = `openid profile email roles ${this.userScope} ${resourceScope}`;
|
|
19
|
-
const codeVerifier = bodhiJsCore.generateCodeVerifier();
|
|
20
|
-
const codeChallenge = await bodhiJsCore.generateCodeChallenge(codeVerifier);
|
|
21
|
-
const state = bodhiJsCore.generateCodeVerifier();
|
|
22
|
-
localStorage.setItem(this.storageKeys.CODE_VERIFIER, codeVerifier);
|
|
23
|
-
localStorage.setItem(this.storageKeys.STATE, state);
|
|
24
|
-
const authUrl = new URL(this.authEndpoints.authorize);
|
|
25
|
-
authUrl.searchParams.set("client_id", this.authClientId);
|
|
26
|
-
authUrl.searchParams.set("response_type", "code");
|
|
27
|
-
authUrl.searchParams.set("redirect_uri", this.redirectUri);
|
|
28
|
-
authUrl.searchParams.set("scope", fullScope);
|
|
29
|
-
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
30
|
-
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
31
|
-
authUrl.searchParams.set("state", state);
|
|
32
|
-
window.location.href = authUrl.toString();
|
|
33
|
-
throw new Error("Redirect initiated");
|
|
34
|
-
}
|
|
35
|
-
async handleOAuthCallback(code, state) {
|
|
36
|
-
const storedState = localStorage.getItem(this.storageKeys.STATE);
|
|
37
|
-
if (!storedState || storedState !== state) {
|
|
38
|
-
throw new Error("Invalid state parameter - possible CSRF attack");
|
|
39
|
-
}
|
|
40
|
-
await this.exchangeCodeForTokens(code);
|
|
41
|
-
localStorage.removeItem(this.storageKeys.CODE_VERIFIER);
|
|
42
|
-
localStorage.removeItem(this.storageKeys.STATE);
|
|
43
|
-
const authState = await this.getAuthState();
|
|
44
|
-
if (!authState.isLoggedIn) {
|
|
45
|
-
throw new Error("Login failed");
|
|
46
|
-
}
|
|
47
|
-
const result = authState;
|
|
48
|
-
this.setAuthState(result);
|
|
49
|
-
return result;
|
|
50
|
-
}
|
|
51
|
-
async logout() {
|
|
52
|
-
const refreshToken = localStorage.getItem(this.storageKeys.REFRESH_TOKEN);
|
|
53
|
-
if (refreshToken) {
|
|
54
|
-
try {
|
|
55
|
-
const params = new URLSearchParams({
|
|
56
|
-
token: refreshToken,
|
|
57
|
-
client_id: this.authClientId,
|
|
58
|
-
token_type_hint: "refresh_token"
|
|
59
|
-
});
|
|
60
|
-
await fetch(this.authEndpoints.revoke, {
|
|
61
|
-
method: "POST",
|
|
62
|
-
headers: {
|
|
63
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
64
|
-
},
|
|
65
|
-
body: params
|
|
66
|
-
});
|
|
67
|
-
} catch (error) {
|
|
68
|
-
this.logger.warn("Token revocation failed:", error);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
localStorage.removeItem(this.storageKeys.ACCESS_TOKEN);
|
|
72
|
-
localStorage.removeItem(this.storageKeys.REFRESH_TOKEN);
|
|
73
|
-
localStorage.removeItem(this.storageKeys.EXPIRES_AT);
|
|
74
|
-
localStorage.removeItem(this.storageKeys.RESOURCE_SCOPE);
|
|
75
|
-
const result = {
|
|
76
|
-
isLoggedIn: false
|
|
77
|
-
};
|
|
78
|
-
this.setAuthState(result);
|
|
79
|
-
return result;
|
|
80
|
-
}
|
|
81
|
-
// ============================================================================
|
|
82
|
-
// OAuth Helper Methods
|
|
83
|
-
// ============================================================================
|
|
84
|
-
async requestResourceAccess() {
|
|
85
|
-
const response = await this.sendApiRequest(
|
|
86
|
-
"POST",
|
|
87
|
-
"/bodhi/v1/apps/request-access",
|
|
88
|
-
{ app_client_id: this.authClientId },
|
|
89
|
-
{},
|
|
90
|
-
false
|
|
91
|
-
);
|
|
92
|
-
if (bodhiJsCore.isApiResultOperationError(response)) {
|
|
93
|
-
throw new Error("Failed to get resource access scope from server");
|
|
94
|
-
}
|
|
95
|
-
if (!bodhiJsCore.isApiResultSuccess(response)) {
|
|
96
|
-
throw new Error("Failed to get resource access scope from server: API error");
|
|
97
|
-
}
|
|
98
|
-
const scope = response.body.scope;
|
|
99
|
-
localStorage.setItem(this.storageKeys.RESOURCE_SCOPE, scope);
|
|
100
|
-
return scope;
|
|
101
|
-
}
|
|
102
|
-
async exchangeCodeForTokens(code) {
|
|
103
|
-
const codeVerifier = localStorage.getItem(this.storageKeys.CODE_VERIFIER);
|
|
104
|
-
if (!codeVerifier) {
|
|
105
|
-
throw new Error("Code verifier not found");
|
|
106
|
-
}
|
|
107
|
-
const response = await fetch(this.authEndpoints.token, {
|
|
108
|
-
method: "POST",
|
|
109
|
-
headers: {
|
|
110
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
111
|
-
},
|
|
112
|
-
body: new URLSearchParams({
|
|
113
|
-
grant_type: "authorization_code",
|
|
114
|
-
code,
|
|
115
|
-
redirect_uri: this.redirectUri,
|
|
116
|
-
client_id: this.authClientId,
|
|
117
|
-
code_verifier: codeVerifier
|
|
118
|
-
})
|
|
119
|
-
});
|
|
120
|
-
if (!response.ok) {
|
|
121
|
-
const errorText = await response.text();
|
|
122
|
-
throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
|
|
123
|
-
}
|
|
124
|
-
const tokens = await response.json();
|
|
125
|
-
localStorage.setItem(this.storageKeys.ACCESS_TOKEN, tokens.access_token);
|
|
126
|
-
if (tokens.refresh_token) {
|
|
127
|
-
localStorage.setItem(this.storageKeys.REFRESH_TOKEN, tokens.refresh_token);
|
|
128
|
-
}
|
|
129
|
-
if (tokens.expires_in) {
|
|
130
|
-
const expiresAt = Date.now() + tokens.expires_in * 1e3;
|
|
131
|
-
localStorage.setItem(this.storageKeys.EXPIRES_AT, expiresAt.toString());
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
// ============================================================================
|
|
135
|
-
// Storage Implementation (localStorage)
|
|
136
|
-
// ============================================================================
|
|
137
|
-
async _storageGet(key) {
|
|
138
|
-
return localStorage.getItem(key);
|
|
139
|
-
}
|
|
140
|
-
async _storageSet(items) {
|
|
141
|
-
Object.entries(items).forEach(([key, value]) => {
|
|
142
|
-
localStorage.setItem(key, String(value));
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
async _storageRemove(keys) {
|
|
146
|
-
keys.forEach((key) => localStorage.removeItem(key));
|
|
147
|
-
}
|
|
148
|
-
_getRedirectUri() {
|
|
149
|
-
return this.redirectUri;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const POLL_INTERVAL = 500;
|
|
153
|
-
const POLL_TIMEOUT = 5e3;
|
|
154
|
-
const STORAGE_KEYS = bodhiJsCore.createStorageKeys(bodhiJsCore.STORAGE_PREFIXES.WEB);
|
|
155
|
-
class WindowBodhiextClient {
|
|
156
|
-
constructor(authClientId, config, onStateChange) {
|
|
157
|
-
this.state = bodhiJsCore.EXTENSION_STATE_NOT_INITIALIZED;
|
|
158
|
-
this.bodhiext = null;
|
|
159
|
-
this.refreshPromise = null;
|
|
160
|
-
this.logger = new bodhiJsCore.Logger("WindowBodhiextClient", config.logLevel);
|
|
161
|
-
this.authClientId = authClientId;
|
|
162
|
-
this.config = config;
|
|
163
|
-
this.authEndpoints = bodhiJsCore.createOAuthEndpoints(this.config.authServerUrl);
|
|
164
|
-
this.onStateChange = onStateChange ?? bodhiJsCore.NOOP_STATE_CALLBACK;
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Set client state and notify callback
|
|
168
|
-
*/
|
|
169
|
-
setState(newState) {
|
|
170
|
-
this.state = newState;
|
|
171
|
-
this.logger.info(`{state: ${JSON.stringify(newState)}} - Setting client state`);
|
|
172
|
-
this.onStateChange({ type: "client-state", state: newState });
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Set auth state and notify callback
|
|
176
|
-
*/
|
|
177
|
-
setAuthState(authState) {
|
|
178
|
-
this.onStateChange({ type: "auth-state", state: authState });
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Set or update the state change callback
|
|
182
|
-
*/
|
|
183
|
-
setStateCallback(callback) {
|
|
184
|
-
this.onStateChange = callback;
|
|
185
|
-
}
|
|
186
|
-
// ============================================================================
|
|
187
|
-
// Extension Communication
|
|
188
|
-
// ============================================================================
|
|
189
|
-
/**
|
|
190
|
-
* Ensure bodhiext is available, attempting to acquire it if not already set
|
|
191
|
-
* @throws Error if client not initialized
|
|
192
|
-
*/
|
|
193
|
-
ensureBodhiext() {
|
|
194
|
-
if (!this.bodhiext && window.bodhiext) {
|
|
195
|
-
this.logger.info("Acquiring window.bodhiext reference");
|
|
196
|
-
this.bodhiext = window.bodhiext;
|
|
197
|
-
}
|
|
198
|
-
if (!this.bodhiext) {
|
|
199
|
-
throw new Error("Client not initialized");
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Send extension request via window.bodhiext.sendExtRequest
|
|
204
|
-
*/
|
|
205
|
-
async sendExtRequest(action, params) {
|
|
206
|
-
this.ensureBodhiext();
|
|
207
|
-
return this.bodhiext.sendExtRequest(action, params);
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Send API message via window.bodhiext.sendApiRequest
|
|
211
|
-
* Converts ApiResponse to ApiResponseResult
|
|
212
|
-
*/
|
|
213
|
-
async sendApiRequest(method, endpoint, body, headers, authenticated) {
|
|
214
|
-
try {
|
|
215
|
-
this.ensureBodhiext();
|
|
216
|
-
} catch (err) {
|
|
217
|
-
return {
|
|
218
|
-
error: {
|
|
219
|
-
message: err instanceof Error ? err.message : String(err),
|
|
220
|
-
type: "extension_error"
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
let requestHeaders = headers || {};
|
|
226
|
-
if (authenticated) {
|
|
227
|
-
const accessToken = await this._getAccessTokenRaw();
|
|
228
|
-
if (!accessToken) {
|
|
229
|
-
return {
|
|
230
|
-
error: {
|
|
231
|
-
message: "Not authenticated. Please log in first.",
|
|
232
|
-
type: "extension_error"
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
requestHeaders = {
|
|
237
|
-
...requestHeaders,
|
|
238
|
-
Authorization: `Bearer ${accessToken}`
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
const response = await this.bodhiext.sendApiRequest(
|
|
242
|
-
method,
|
|
243
|
-
endpoint,
|
|
244
|
-
body,
|
|
245
|
-
requestHeaders
|
|
246
|
-
);
|
|
247
|
-
return response;
|
|
248
|
-
} catch (e) {
|
|
249
|
-
const errorObj = e == null ? void 0 : e.error;
|
|
250
|
-
const message = (errorObj == null ? void 0 : errorObj.message) ?? (e instanceof Error ? e.message : String(e));
|
|
251
|
-
const errorType = (errorObj == null ? void 0 : errorObj.type) || "extension_error";
|
|
252
|
-
return {
|
|
253
|
-
error: {
|
|
254
|
-
message,
|
|
255
|
-
type: errorType
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Get current client state
|
|
262
|
-
*/
|
|
263
|
-
getState() {
|
|
264
|
-
return this.state;
|
|
265
|
-
}
|
|
266
|
-
isClientInitialized() {
|
|
267
|
-
return this.state.extension === "ready";
|
|
268
|
-
}
|
|
269
|
-
isServerReady() {
|
|
270
|
-
return this.isClientInitialized() && this.state.server.status === "ready";
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Initialize extension discovery with optional timeout
|
|
274
|
-
* Returns ExtensionState with extension and server status
|
|
275
|
-
*
|
|
276
|
-
* Note: Web mode uses stateless discovery (always polls for window.bodhiext)
|
|
277
|
-
* No extensionId storage/restoration needed - window.bodhiext handle is ephemeral
|
|
278
|
-
*/
|
|
279
|
-
async init(params = {}) {
|
|
280
|
-
var _a, _b, _c, _d;
|
|
281
|
-
if (!params.testConnection && !params.selectedConnection) {
|
|
282
|
-
this.logger.info("No testConnection or selectedConnection, returning not-initialized state");
|
|
283
|
-
return bodhiJsCore.EXTENSION_STATE_NOT_INITIALIZED;
|
|
284
|
-
}
|
|
285
|
-
if (this.bodhiext && !params.testConnection) {
|
|
286
|
-
this.logger.debug("Already have bodhiext handle, skipping polling");
|
|
287
|
-
return this.state;
|
|
288
|
-
}
|
|
289
|
-
if (!this.bodhiext) {
|
|
290
|
-
const timeoutMs = params.timeoutMs ?? ((_b = (_a = this.config.initParams) == null ? void 0 : _a.extension) == null ? void 0 : _b.timeoutMs) ?? POLL_TIMEOUT;
|
|
291
|
-
const intervalMs = params.intervalMs ?? ((_d = (_c = this.config.initParams) == null ? void 0 : _c.extension) == null ? void 0 : _d.intervalMs) ?? POLL_INTERVAL;
|
|
292
|
-
const startTime = Date.now();
|
|
293
|
-
const found = await new Promise((resolve) => {
|
|
294
|
-
const check = () => {
|
|
295
|
-
if (window.bodhiext) {
|
|
296
|
-
this.bodhiext = window.bodhiext;
|
|
297
|
-
resolve(true);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
if (Date.now() - startTime >= timeoutMs) {
|
|
301
|
-
resolve(false);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
setTimeout(check, intervalMs);
|
|
305
|
-
};
|
|
306
|
-
check();
|
|
307
|
-
});
|
|
308
|
-
if (!found) {
|
|
309
|
-
this.logger.warn(`Extension discovery timed out`);
|
|
310
|
-
this.setState(bodhiJsCore.EXTENSION_STATE_NOT_FOUND);
|
|
311
|
-
return this.state;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
const extensionId = await this.bodhiext.getExtensionId();
|
|
315
|
-
this.logger.info(`Extension discovered: ${extensionId}`);
|
|
316
|
-
const state = {
|
|
317
|
-
type: "extension",
|
|
318
|
-
extension: "ready",
|
|
319
|
-
extensionId,
|
|
320
|
-
server: bodhiJsCore.PENDING_EXTENSION_READY
|
|
321
|
-
};
|
|
322
|
-
if (params.testConnection) {
|
|
323
|
-
try {
|
|
324
|
-
const serverState = await this.getServerState();
|
|
325
|
-
this.setState({ ...state, server: serverState });
|
|
326
|
-
this.logger.info(`Server connectivity tested: ${serverState.status}`);
|
|
327
|
-
} catch (error) {
|
|
328
|
-
this.logger.error(`Failed to get server state:`, error);
|
|
329
|
-
this.setState({ ...state, server: bodhiJsCore.BACKEND_SERVER_NOT_REACHABLE });
|
|
330
|
-
}
|
|
331
|
-
} else {
|
|
332
|
-
this.setState(state);
|
|
333
|
-
}
|
|
334
|
-
return this.state;
|
|
335
|
-
}
|
|
336
|
-
// ============================================================================
|
|
337
|
-
// OAuth Methods
|
|
338
|
-
// ============================================================================
|
|
339
|
-
/**
|
|
340
|
-
* Request resource access scope from backend
|
|
341
|
-
* Required for authenticated API access
|
|
342
|
-
*/
|
|
343
|
-
async requestResourceAccess() {
|
|
344
|
-
this.ensureBodhiext();
|
|
345
|
-
const response = await this.bodhiext.sendApiRequest("POST", "/bodhi/v1/apps/request-access", {
|
|
346
|
-
app_client_id: this.authClientId
|
|
347
|
-
});
|
|
348
|
-
if (!bodhiJsCore.isApiResultSuccess(response)) {
|
|
349
|
-
throw new Error("Failed to get resource access scope: API error");
|
|
350
|
-
}
|
|
351
|
-
const scope = response.body.scope;
|
|
352
|
-
localStorage.setItem(STORAGE_KEYS.RESOURCE_SCOPE, scope);
|
|
353
|
-
return scope;
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Login via browser redirect OAuth2 + PKCE flow
|
|
357
|
-
* @returns AuthLoggedIn (though in practice, this redirects and never returns)
|
|
358
|
-
*/
|
|
359
|
-
async login() {
|
|
360
|
-
const existingAuth = await this.getAuthState();
|
|
361
|
-
if (existingAuth.isLoggedIn) {
|
|
362
|
-
return existingAuth;
|
|
363
|
-
}
|
|
364
|
-
this.ensureBodhiext();
|
|
365
|
-
const resourceScope = await this.requestResourceAccess();
|
|
366
|
-
const codeVerifier = bodhiJsCore.generateCodeVerifier();
|
|
367
|
-
const codeChallenge = await bodhiJsCore.generateCodeChallenge(codeVerifier);
|
|
368
|
-
const state = bodhiJsCore.generateCodeVerifier();
|
|
369
|
-
localStorage.setItem(STORAGE_KEYS.CODE_VERIFIER, codeVerifier);
|
|
370
|
-
localStorage.setItem(STORAGE_KEYS.STATE, state);
|
|
371
|
-
const scopes = ["openid", "profile", "email", "roles", this.config.userScope, resourceScope];
|
|
372
|
-
const params = new URLSearchParams({
|
|
373
|
-
response_type: "code",
|
|
374
|
-
client_id: this.authClientId,
|
|
375
|
-
redirect_uri: this.config.redirectUri,
|
|
376
|
-
scope: scopes.join(" "),
|
|
377
|
-
state,
|
|
378
|
-
code_challenge: codeChallenge,
|
|
379
|
-
code_challenge_method: "S256"
|
|
380
|
-
});
|
|
381
|
-
const authUrl = `${this.authEndpoints.authorize}?${params}`;
|
|
382
|
-
window.location.href = authUrl;
|
|
383
|
-
return new Promise(() => {
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Handle OAuth callback with authorization code
|
|
388
|
-
* Should be called from callback page with extracted URL params
|
|
389
|
-
* @returns AuthLoggedIn with login state and user info
|
|
390
|
-
*/
|
|
391
|
-
async handleOAuthCallback(code, state) {
|
|
392
|
-
const storedState = localStorage.getItem(STORAGE_KEYS.STATE);
|
|
393
|
-
if (!storedState || storedState !== state) {
|
|
394
|
-
throw new Error("Invalid state parameter - possible CSRF attack");
|
|
395
|
-
}
|
|
396
|
-
await this.exchangeCodeForTokens(code);
|
|
397
|
-
localStorage.removeItem(STORAGE_KEYS.CODE_VERIFIER);
|
|
398
|
-
localStorage.removeItem(STORAGE_KEYS.STATE);
|
|
399
|
-
const authState = await this.getAuthState();
|
|
400
|
-
if (!authState.isLoggedIn) {
|
|
401
|
-
throw new Error("Login failed");
|
|
402
|
-
}
|
|
403
|
-
this.setAuthState(authState);
|
|
404
|
-
return authState;
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Exchange authorization code for tokens
|
|
408
|
-
*/
|
|
409
|
-
async exchangeCodeForTokens(code) {
|
|
410
|
-
const codeVerifier = localStorage.getItem(STORAGE_KEYS.CODE_VERIFIER);
|
|
411
|
-
if (!codeVerifier) {
|
|
412
|
-
throw new Error("Code verifier not found");
|
|
413
|
-
}
|
|
414
|
-
const params = new URLSearchParams({
|
|
415
|
-
grant_type: "authorization_code",
|
|
416
|
-
client_id: this.authClientId,
|
|
417
|
-
code,
|
|
418
|
-
redirect_uri: this.config.redirectUri,
|
|
419
|
-
code_verifier: codeVerifier
|
|
420
|
-
});
|
|
421
|
-
const response = await fetch(this.authEndpoints.token, {
|
|
422
|
-
method: "POST",
|
|
423
|
-
headers: {
|
|
424
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
425
|
-
},
|
|
426
|
-
body: params
|
|
427
|
-
});
|
|
428
|
-
if (!response.ok) {
|
|
429
|
-
const errorText = await response.text();
|
|
430
|
-
throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
|
|
431
|
-
}
|
|
432
|
-
const tokenData = await response.json();
|
|
433
|
-
if (!tokenData.access_token) {
|
|
434
|
-
throw new Error("No access token received");
|
|
435
|
-
}
|
|
436
|
-
localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, tokenData.access_token);
|
|
437
|
-
if (tokenData.refresh_token) {
|
|
438
|
-
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, tokenData.refresh_token);
|
|
439
|
-
}
|
|
440
|
-
if (tokenData.expires_in) {
|
|
441
|
-
const expiresAt = Date.now() + tokenData.expires_in * 1e3;
|
|
442
|
-
localStorage.setItem(STORAGE_KEYS.EXPIRES_AT, expiresAt.toString());
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* Logout user and revoke tokens
|
|
447
|
-
* @returns AuthLoggedOut with logged out state
|
|
448
|
-
*/
|
|
449
|
-
async logout() {
|
|
450
|
-
const refreshToken = localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
451
|
-
if (refreshToken) {
|
|
452
|
-
try {
|
|
453
|
-
const params = new URLSearchParams({
|
|
454
|
-
token: refreshToken,
|
|
455
|
-
client_id: this.authClientId,
|
|
456
|
-
token_type_hint: "refresh_token"
|
|
457
|
-
});
|
|
458
|
-
await fetch(this.authEndpoints.revoke, {
|
|
459
|
-
method: "POST",
|
|
460
|
-
headers: {
|
|
461
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
462
|
-
},
|
|
463
|
-
body: params
|
|
464
|
-
});
|
|
465
|
-
} catch (error) {
|
|
466
|
-
this.logger.warn("Token revocation failed:", error);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
470
|
-
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
471
|
-
localStorage.removeItem(STORAGE_KEYS.EXPIRES_AT);
|
|
472
|
-
localStorage.removeItem(STORAGE_KEYS.CODE_VERIFIER);
|
|
473
|
-
localStorage.removeItem(STORAGE_KEYS.STATE);
|
|
474
|
-
localStorage.removeItem(STORAGE_KEYS.RESOURCE_SCOPE);
|
|
475
|
-
const result = {
|
|
476
|
-
isLoggedIn: false
|
|
477
|
-
};
|
|
478
|
-
this.setAuthState(result);
|
|
479
|
-
return result;
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Get current authentication state
|
|
483
|
-
*/
|
|
484
|
-
async getAuthState() {
|
|
485
|
-
const accessToken = await this._getAccessTokenRaw();
|
|
486
|
-
if (!accessToken) {
|
|
487
|
-
return { isLoggedIn: false };
|
|
488
|
-
}
|
|
489
|
-
try {
|
|
490
|
-
const userInfo = bodhiJsCore.extractUserInfo(accessToken);
|
|
491
|
-
return { isLoggedIn: true, userInfo, accessToken };
|
|
492
|
-
} catch (error) {
|
|
493
|
-
this.logger.error("Failed to parse token:", error);
|
|
494
|
-
return { isLoggedIn: false };
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Get current access token
|
|
499
|
-
* Returns null if not logged in or token expired
|
|
500
|
-
*/
|
|
501
|
-
async _getAccessTokenRaw() {
|
|
502
|
-
const accessToken = localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
503
|
-
const expiresAt = localStorage.getItem(STORAGE_KEYS.EXPIRES_AT);
|
|
504
|
-
if (!accessToken) {
|
|
505
|
-
return null;
|
|
506
|
-
}
|
|
507
|
-
if (expiresAt) {
|
|
508
|
-
const expirationTime = parseInt(expiresAt, 10);
|
|
509
|
-
if (Date.now() >= expirationTime - 5 * 1e3) {
|
|
510
|
-
const refreshToken = localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
511
|
-
if (refreshToken) {
|
|
512
|
-
return this._tryRefreshToken(refreshToken);
|
|
513
|
-
}
|
|
514
|
-
return null;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
return accessToken;
|
|
518
|
-
}
|
|
519
|
-
/**
|
|
520
|
-
* Try to refresh access token using refresh token
|
|
521
|
-
* Race condition prevention: Returns existing promise if refresh already in progress
|
|
522
|
-
*/
|
|
523
|
-
async _tryRefreshToken(refreshToken) {
|
|
524
|
-
if (this.refreshPromise) {
|
|
525
|
-
this.logger.debug("Refresh already in progress, returning existing promise");
|
|
526
|
-
return this.refreshPromise;
|
|
527
|
-
}
|
|
528
|
-
this.refreshPromise = this._doRefreshToken(refreshToken);
|
|
529
|
-
try {
|
|
530
|
-
return await this.refreshPromise;
|
|
531
|
-
} finally {
|
|
532
|
-
this.refreshPromise = null;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* Perform the actual token refresh
|
|
537
|
-
*/
|
|
538
|
-
async _doRefreshToken(refreshToken) {
|
|
539
|
-
this.logger.debug("Refreshing access token");
|
|
540
|
-
try {
|
|
541
|
-
const tokens = await bodhiJsCore.refreshAccessToken(
|
|
542
|
-
this.authEndpoints.token,
|
|
543
|
-
refreshToken,
|
|
544
|
-
this.authClientId
|
|
545
|
-
);
|
|
546
|
-
if (tokens) {
|
|
547
|
-
this._storeRefreshedTokens(tokens);
|
|
548
|
-
const userInfo = bodhiJsCore.extractUserInfo(tokens.access_token);
|
|
549
|
-
this.setAuthState({
|
|
550
|
-
isLoggedIn: true,
|
|
551
|
-
userInfo,
|
|
552
|
-
accessToken: tokens.access_token
|
|
553
|
-
});
|
|
554
|
-
this.logger.info("Token refreshed successfully");
|
|
555
|
-
return tokens.access_token;
|
|
556
|
-
}
|
|
557
|
-
} catch (error) {
|
|
558
|
-
this.logger.warn("Token refresh failed:", error);
|
|
559
|
-
}
|
|
560
|
-
this.logger.warn("Token refresh failed, keeping tokens for manual retry");
|
|
561
|
-
throw bodhiJsCore.createOperationError(
|
|
562
|
-
"Access token expired and unable to refresh. Try logging out and logging in again.",
|
|
563
|
-
"token_refresh_failed"
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
/**
|
|
567
|
-
* Store refreshed tokens
|
|
568
|
-
*/
|
|
569
|
-
_storeRefreshedTokens(tokens) {
|
|
570
|
-
const expiresAt = Date.now() + tokens.expires_in * 1e3;
|
|
571
|
-
localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, tokens.access_token);
|
|
572
|
-
localStorage.setItem(STORAGE_KEYS.EXPIRES_AT, String(expiresAt));
|
|
573
|
-
if (tokens.refresh_token) {
|
|
574
|
-
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, tokens.refresh_token);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Ping API
|
|
579
|
-
*/
|
|
580
|
-
async pingApi() {
|
|
581
|
-
return this.sendApiRequest("GET", "/ping");
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Fetch models
|
|
585
|
-
*/
|
|
586
|
-
async fetchModels() {
|
|
587
|
-
return this.sendApiRequest(
|
|
588
|
-
"GET",
|
|
589
|
-
"/v1/models",
|
|
590
|
-
void 0,
|
|
591
|
-
void 0,
|
|
592
|
-
true
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* Get backend server state
|
|
597
|
-
* Calls /bodhi/v1/info and returns structured server state
|
|
598
|
-
*/
|
|
599
|
-
async getServerState() {
|
|
600
|
-
const result = await this.sendApiRequest("GET", "/bodhi/v1/info");
|
|
601
|
-
if (bodhiJsCore.isApiResultOperationError(result)) {
|
|
602
|
-
return bodhiJsCore.BACKEND_SERVER_NOT_REACHABLE;
|
|
603
|
-
}
|
|
604
|
-
if (!bodhiJsCore.isApiResultSuccess(result)) {
|
|
605
|
-
return bodhiJsCore.BACKEND_SERVER_NOT_REACHABLE;
|
|
606
|
-
}
|
|
607
|
-
const body = result.body;
|
|
608
|
-
switch (body.status) {
|
|
609
|
-
case "ready":
|
|
610
|
-
return { status: "ready", version: body.version || "unknown" };
|
|
611
|
-
case "setup":
|
|
612
|
-
return bodhiJsCore.backendServerNotReady("setup", body.version || "unknown");
|
|
613
|
-
case "resource-admin":
|
|
614
|
-
return bodhiJsCore.backendServerNotReady("resource-admin", body.version || "unknown");
|
|
615
|
-
case "error":
|
|
616
|
-
return bodhiJsCore.backendServerNotReady(
|
|
617
|
-
"error",
|
|
618
|
-
body.version || "unknown",
|
|
619
|
-
body.error ? { message: body.error.message, type: body.error.type } : bodhiJsCore.SERVER_ERROR_CODES.SERVER_NOT_READY
|
|
620
|
-
);
|
|
621
|
-
default:
|
|
622
|
-
return bodhiJsCore.BACKEND_SERVER_NOT_REACHABLE;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
/**
|
|
626
|
-
* Generic streaming via window.bodhiext.sendStreamRequest
|
|
627
|
-
* Wraps ReadableStream as AsyncGenerator
|
|
628
|
-
*/
|
|
629
|
-
async *stream(method, endpoint, body, headers, authenticated = true) {
|
|
630
|
-
this.ensureBodhiext();
|
|
631
|
-
let requestHeaders = headers || {};
|
|
632
|
-
if (authenticated) {
|
|
633
|
-
const accessToken = await this._getAccessTokenRaw();
|
|
634
|
-
if (!accessToken) {
|
|
635
|
-
throw new Error("Not authenticated. Please log in first.");
|
|
636
|
-
}
|
|
637
|
-
requestHeaders = {
|
|
638
|
-
...requestHeaders,
|
|
639
|
-
Authorization: `Bearer ${accessToken}`
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
const stream = this.bodhiext.sendStreamRequest(method, endpoint, body, requestHeaders);
|
|
643
|
-
const reader = stream.getReader();
|
|
644
|
-
try {
|
|
645
|
-
while (true) {
|
|
646
|
-
const { value, done } = await reader.read();
|
|
647
|
-
if (done || (value == null ? void 0 : value.done)) {
|
|
648
|
-
break;
|
|
649
|
-
}
|
|
650
|
-
yield value.body;
|
|
651
|
-
}
|
|
652
|
-
} catch (err) {
|
|
653
|
-
if (err instanceof Error) {
|
|
654
|
-
if ("response" in err) {
|
|
655
|
-
const apiErr = err;
|
|
656
|
-
throw bodhiJsCore.createApiError(err.message, apiErr.response.status, apiErr.response.body);
|
|
657
|
-
}
|
|
658
|
-
if ("error" in err) {
|
|
659
|
-
throw bodhiJsCore.createOperationError(err.message, "extension_error");
|
|
660
|
-
}
|
|
661
|
-
throw bodhiJsCore.createOperationError(err.message, "extension_error");
|
|
662
|
-
}
|
|
663
|
-
throw err;
|
|
664
|
-
} finally {
|
|
665
|
-
reader.releaseLock();
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Chat streaming
|
|
670
|
-
*/
|
|
671
|
-
async *streamChat(model, prompt, authenticated = true) {
|
|
672
|
-
yield* this.stream(
|
|
673
|
-
"POST",
|
|
674
|
-
"/v1/chat/completions",
|
|
675
|
-
{
|
|
676
|
-
model,
|
|
677
|
-
messages: [{ role: "user", content: prompt }],
|
|
678
|
-
stream: true
|
|
679
|
-
},
|
|
680
|
-
void 0,
|
|
681
|
-
authenticated
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
/**
|
|
685
|
-
* Serialize web extension client state (all transient, nothing to persist)
|
|
686
|
-
*/
|
|
687
|
-
serialize() {
|
|
688
|
-
return {
|
|
689
|
-
extensionId: this.state.type === "extension" && this.state.extension === "ready" ? this.state.extensionId : void 0
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* Debug dump of WindowBodhiextClient internal state
|
|
694
|
-
*/
|
|
695
|
-
async debug() {
|
|
696
|
-
return {
|
|
697
|
-
type: "WindowBodhiextClient",
|
|
698
|
-
state: this.state,
|
|
699
|
-
authState: await this.getAuthState(),
|
|
700
|
-
bodhiextAvailable: this.bodhiext !== null,
|
|
701
|
-
authClientId: this.authClientId,
|
|
702
|
-
authServerUrl: this.config.authServerUrl,
|
|
703
|
-
redirectUri: this.config.redirectUri,
|
|
704
|
-
userScope: this.config.userScope
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
class WebUIClient extends bodhiJsCore.BaseFacadeClient {
|
|
709
|
-
constructor(authClientId, config, onStateChange, storagePrefix) {
|
|
710
|
-
const normalizedConfig = {
|
|
711
|
-
redirectUri: config.redirectUri,
|
|
712
|
-
authServerUrl: config.authServerUrl || "https://id.getbodhi.app/realms/bodhi",
|
|
713
|
-
userScope: config.userScope || "scope_user_user",
|
|
714
|
-
logLevel: config.logLevel || "warn",
|
|
715
|
-
initParams: config.initParams
|
|
716
|
-
};
|
|
717
|
-
super(authClientId, normalizedConfig, onStateChange, storagePrefix);
|
|
718
|
-
}
|
|
719
|
-
createLogger(config) {
|
|
720
|
-
return new bodhiJsCore.Logger("WebUIClient", config.logLevel);
|
|
721
|
-
}
|
|
722
|
-
createExtClient(config, onStateChange) {
|
|
723
|
-
return new WindowBodhiextClient(this.authClientId, config, onStateChange);
|
|
724
|
-
}
|
|
725
|
-
createDirectClient(authClientId, config, onStateChange) {
|
|
726
|
-
return new DirectWebClient(
|
|
727
|
-
{
|
|
728
|
-
authClientId,
|
|
729
|
-
authServerUrl: config.authServerUrl,
|
|
730
|
-
redirectUri: config.redirectUri,
|
|
731
|
-
userScope: config.userScope,
|
|
732
|
-
logLevel: config.logLevel,
|
|
733
|
-
storagePrefix: bodhiJsCore.STORAGE_PREFIXES.WEB
|
|
734
|
-
},
|
|
735
|
-
onStateChange
|
|
736
|
-
);
|
|
737
|
-
}
|
|
738
|
-
// ============================================================================
|
|
739
|
-
// Web-specific OAuth Callback
|
|
740
|
-
// ============================================================================
|
|
741
|
-
async handleOAuthCallback(code, state) {
|
|
742
|
-
if (this.connectionMode === "direct") {
|
|
743
|
-
return this.directClient.handleOAuthCallback(code, state);
|
|
744
|
-
}
|
|
745
|
-
return this.extClient.handleOAuthCallback(code, state);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
exports.WebUIClient = WebUIClient;
|
|
749
|
-
//# sourceMappingURL=bodhi-web.cjs.js.map
|
|
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;
|