@bodhiapp/bodhi-js-ext 0.0.1 → 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.
@@ -1,1911 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const bodhiJsCore = require("@bodhiapp/bodhi-js-core");
4
- const EXT2EXT_CLIENT_MESSAGE_TYPES = {
5
- EXT2EXT_CLIENT_REQUEST: "EXT2EXT_CLIENT_REQUEST",
6
- EXT2EXT_CLIENT_RESPONSE: "EXT2EXT_CLIENT_RESPONSE",
7
- EXT2EXT_CLIENT_BROADCAST: "EXT2EXT_CLIENT_BROADCAST",
8
- EXT2EXT_CLIENT_API_REQUEST: "EXT2EXT_CLIENT_API_REQUEST",
9
- EXT2EXT_CLIENT_API_RESPONSE: "EXT2EXT_CLIENT_API_RESPONSE",
10
- // Streaming message types (UI → background)
11
- EXT2EXT_CLIENT_STREAM_REQUEST: "EXT2EXT_CLIENT_STREAM_REQUEST",
12
- EXT2EXT_CLIENT_STREAM_CHUNK: "EXT2EXT_CLIENT_STREAM_CHUNK",
13
- EXT2EXT_CLIENT_STREAM_ERROR: "EXT2EXT_CLIENT_STREAM_ERROR",
14
- EXT2EXT_CLIENT_STREAM_API_ERROR: "EXT2EXT_CLIENT_STREAM_API_ERROR",
15
- EXT2EXT_CLIENT_STREAM_DONE: "EXT2EXT_CLIENT_STREAM_DONE"
16
- };
17
- const EXT2EXT_CLIENT_STREAM_PORT = "ext2ext-client-stream";
18
- const EXT2EXT_CLIENT_ACTIONS = {
19
- LOGIN: "login",
20
- LOGOUT: "logout",
21
- GET_AUTH_STATE: "getAuthState",
22
- DISCOVER_EXTENSION: "discoverBodhiExtension",
23
- GET_EXTENSION_ID: "get_extension_id",
24
- SET_EXTENSION_ID: "setExtensionId"
25
- };
26
- const DISCOVERY_TIMEOUT_MS = 5e3;
27
- const DISCOVERY_ATTEMPTS = 3;
28
- const DISCOVERY_ATTEMPT_WAIT_MS = 500;
29
- const DISCOVERY_ATTEMPT_TIMEOUT = 500;
30
- function isExtClientApiError(msg) {
31
- return "error" in msg;
32
- }
33
- class DirectExtClient extends bodhiJsCore.DirectClientBase {
34
- constructor(config, onStateChange) {
35
- super({ ...config, storagePrefix: bodhiJsCore.STORAGE_PREFIXES.EXT }, "DirectExtClient", onStateChange);
36
- }
37
- // ============================================================================
38
- // Authentication (chrome.identity OAuth)
39
- // ============================================================================
40
- async login() {
41
- const existingAuth = await this.getAuthState();
42
- if (existingAuth.isLoggedIn) {
43
- return existingAuth;
44
- }
45
- const resourceScope = await this.requestResourceAccess();
46
- const fullScope = `openid profile email roles ${this.userScope} ${resourceScope}`;
47
- const codeVerifier = bodhiJsCore.generateCodeVerifier();
48
- const codeChallenge = await bodhiJsCore.generateCodeChallenge(codeVerifier);
49
- const state = bodhiJsCore.generateCodeVerifier();
50
- await chrome.storage.session.set({
51
- [this.storageKeys.CODE_VERIFIER]: codeVerifier,
52
- [this.storageKeys.STATE]: state
53
- });
54
- const redirectUri = chrome.identity.getRedirectURL("callback");
55
- const authUrl = new URL(this.authEndpoints.authorize);
56
- authUrl.searchParams.set("client_id", this.authClientId);
57
- authUrl.searchParams.set("response_type", "code");
58
- authUrl.searchParams.set("redirect_uri", redirectUri);
59
- authUrl.searchParams.set("scope", fullScope);
60
- authUrl.searchParams.set("code_challenge", codeChallenge);
61
- authUrl.searchParams.set("code_challenge_method", "S256");
62
- authUrl.searchParams.set("state", state);
63
- return new Promise((resolve, reject) => {
64
- chrome.identity.launchWebAuthFlow(
65
- {
66
- url: authUrl.toString(),
67
- interactive: true
68
- },
69
- async (redirectUrl) => {
70
- if (chrome.runtime.lastError) {
71
- await chrome.storage.session.remove([
72
- this.storageKeys.CODE_VERIFIER,
73
- this.storageKeys.STATE
74
- ]);
75
- reject(chrome.runtime.lastError);
76
- return;
77
- }
78
- if (!redirectUrl) {
79
- await chrome.storage.session.remove([
80
- this.storageKeys.CODE_VERIFIER,
81
- this.storageKeys.STATE
82
- ]);
83
- reject(bodhiJsCore.createOperationError("No redirect URL received", "oauth-error"));
84
- return;
85
- }
86
- try {
87
- const url = new URL(redirectUrl);
88
- const code = url.searchParams.get("code");
89
- const returnedState = url.searchParams.get("state");
90
- const data = await chrome.storage.session.get(this.storageKeys.STATE);
91
- const savedState = data[this.storageKeys.STATE];
92
- if (returnedState !== savedState) {
93
- await chrome.storage.session.remove([
94
- this.storageKeys.CODE_VERIFIER,
95
- this.storageKeys.STATE
96
- ]);
97
- reject(bodhiJsCore.createOperationError("State mismatch - possible CSRF", "oauth-error"));
98
- return;
99
- }
100
- if (!code) {
101
- await chrome.storage.session.remove([
102
- this.storageKeys.CODE_VERIFIER,
103
- this.storageKeys.STATE
104
- ]);
105
- reject(bodhiJsCore.createOperationError("No authorization code received", "oauth-error"));
106
- return;
107
- }
108
- await this.exchangeCodeForTokens(code);
109
- const authState = await this.getAuthState();
110
- if (!authState.isLoggedIn) {
111
- throw bodhiJsCore.createOperationError("Login failed", "oauth-error");
112
- }
113
- const result = authState;
114
- this.setAuthState(result);
115
- await chrome.storage.session.remove([
116
- this.storageKeys.CODE_VERIFIER,
117
- this.storageKeys.STATE
118
- ]);
119
- resolve(result);
120
- } catch (error) {
121
- await chrome.storage.session.remove([
122
- this.storageKeys.CODE_VERIFIER,
123
- this.storageKeys.STATE
124
- ]);
125
- reject(error);
126
- }
127
- }
128
- );
129
- });
130
- }
131
- async logout() {
132
- const data = await chrome.storage.session.get(this.storageKeys.REFRESH_TOKEN);
133
- const refreshToken = data[this.storageKeys.REFRESH_TOKEN];
134
- if (refreshToken) {
135
- try {
136
- const params = new URLSearchParams({
137
- token: refreshToken,
138
- client_id: this.authClientId,
139
- token_type_hint: "refresh_token"
140
- });
141
- await fetch(this.authEndpoints.revoke, {
142
- method: "POST",
143
- headers: {
144
- "Content-Type": "application/x-www-form-urlencoded"
145
- },
146
- body: params
147
- });
148
- } catch (error) {
149
- this.logger.warn("Token revocation failed:", error);
150
- }
151
- }
152
- await chrome.storage.session.remove([
153
- this.storageKeys.ACCESS_TOKEN,
154
- this.storageKeys.REFRESH_TOKEN,
155
- this.storageKeys.EXPIRES_AT,
156
- this.storageKeys.RESOURCE_SCOPE
157
- ]);
158
- const result = {
159
- isLoggedIn: false
160
- };
161
- this.setAuthState(result);
162
- return result;
163
- }
164
- // ============================================================================
165
- // OAuth Helper Methods
166
- // ============================================================================
167
- async requestResourceAccess() {
168
- const response = await this.sendApiRequest(
169
- "POST",
170
- "/bodhi/v1/apps/request-access",
171
- { app_client_id: this.authClientId },
172
- {},
173
- false
174
- );
175
- if (bodhiJsCore.isApiResultOperationError(response)) {
176
- throw new Error("Failed to get resource access scope from server");
177
- }
178
- if (!bodhiJsCore.isApiResultSuccess(response)) {
179
- throw new Error("Failed to get resource access scope from server: API error");
180
- }
181
- const scope = response.body.scope;
182
- await chrome.storage.session.set({ [this.storageKeys.RESOURCE_SCOPE]: scope });
183
- return scope;
184
- }
185
- async exchangeCodeForTokens(code) {
186
- const data = await chrome.storage.session.get(this.storageKeys.CODE_VERIFIER);
187
- const codeVerifier = data[this.storageKeys.CODE_VERIFIER];
188
- const redirectUri = chrome.identity.getRedirectURL("callback");
189
- const response = await fetch(this.authEndpoints.token, {
190
- method: "POST",
191
- headers: {
192
- "Content-Type": "application/x-www-form-urlencoded"
193
- },
194
- body: new URLSearchParams({
195
- grant_type: "authorization_code",
196
- code,
197
- redirect_uri: redirectUri,
198
- client_id: this.authClientId,
199
- code_verifier: codeVerifier
200
- })
201
- });
202
- if (!response.ok) {
203
- const errorText = await response.text();
204
- throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
205
- }
206
- const tokens = await response.json();
207
- const expiresAt = Date.now() + (tokens.expires_in || 3600) * 1e3;
208
- await chrome.storage.session.set({
209
- [this.storageKeys.ACCESS_TOKEN]: tokens.access_token,
210
- [this.storageKeys.REFRESH_TOKEN]: tokens.refresh_token,
211
- [this.storageKeys.EXPIRES_AT]: expiresAt
212
- });
213
- await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER, this.storageKeys.STATE]);
214
- }
215
- // ============================================================================
216
- // Storage Implementation (chrome.storage.session)
217
- // ============================================================================
218
- async _storageGet(key) {
219
- const data = await chrome.storage.session.get(key);
220
- const value = data[key];
221
- return value !== void 0 ? String(value) : null;
222
- }
223
- async _storageSet(items) {
224
- await chrome.storage.session.set(items);
225
- }
226
- async _storageRemove(keys) {
227
- await chrome.storage.session.remove(keys);
228
- }
229
- _getRedirectUri() {
230
- return chrome.identity.getRedirectURL("callback");
231
- }
232
- }
233
- function isNonNullObject(value) {
234
- return value !== null && typeof value === "object";
235
- }
236
- function isOperationErrorStructure(obj) {
237
- return isNonNullObject(obj) && "message" in obj && typeof obj.message === "string" && "type" in obj && typeof obj.type === "string";
238
- }
239
- const MESSAGE_TYPES = {
240
- API_REQUEST: "BODHI_API_REQUEST",
241
- API_RESPONSE: "BODHI_API_RESPONSE",
242
- STREAM_REQUEST: "BODHI_STREAM_REQUEST",
243
- STREAM_CHUNK: "BODHI_STREAM_CHUNK",
244
- STREAM_ERROR: "BODHI_STREAM_ERROR",
245
- STREAM_API_ERROR: "BODHI_STREAM_API_ERROR",
246
- ERROR: "BODHI_ERROR",
247
- EXT_REQUEST: "BODHI_EXT_REQUEST",
248
- EXT_RESPONSE: "BODHI_EXT_RESPONSE"
249
- };
250
- function isApiSuccessResponse(response) {
251
- return response !== null && typeof response === "object" && typeof response.status === "number" && response.status >= 200 && response.status < 300 && "body" in response;
252
- }
253
- function isStreamChunk(msg) {
254
- return msg !== null && typeof msg === "object" && msg.type === MESSAGE_TYPES.STREAM_CHUNK;
255
- }
256
- function isStreamApiError(msg) {
257
- return msg !== null && typeof msg === "object" && msg.type === MESSAGE_TYPES.STREAM_API_ERROR;
258
- }
259
- function isStreamError(msg) {
260
- return msg !== null && typeof msg === "object" && msg.type === MESSAGE_TYPES.STREAM_ERROR;
261
- }
262
- function isExtError(res) {
263
- return res !== null && typeof res === "object" && "error" in res;
264
- }
265
- function isOperationError(err) {
266
- return err instanceof Error && "error" in err && !("response" in err) && isOperationErrorStructure(err.error);
267
- }
268
- const EXT_ACTIONS = {
269
- GET_EXTENSION_ID: "get_extension_id",
270
- TEST_CONNECTION: "test_connection"
271
- };
272
- const BODHI_STREAM_PORT = "BODHI_STREAM_PORT";
273
- class ExtClient {
274
- constructor(config = {}, onStateChange) {
275
- this.state = {
276
- type: "extension",
277
- extension: "not-initialized",
278
- server: bodhiJsCore.PENDING_EXTENSION_READY
279
- };
280
- this.extensionId = null;
281
- this.broadcastListenerActive = false;
282
- this.config = config;
283
- this.logger = new bodhiJsCore.Logger("ExtClient", (config == null ? void 0 : config.logLevel) || "warn");
284
- this.onStateChange = onStateChange ?? bodhiJsCore.NOOP_STATE_CALLBACK;
285
- }
286
- /**
287
- * Set client state and notify callback
288
- */
289
- setState(newState) {
290
- this.state = newState;
291
- this.onStateChange({ type: "client-state", state: newState });
292
- }
293
- /**
294
- * Set auth state and notify callback
295
- */
296
- setAuthState(authState) {
297
- this.onStateChange({ type: "auth-state", state: authState });
298
- }
299
- /**
300
- * Set or update the state change callback
301
- */
302
- setStateCallback(callback) {
303
- this.onStateChange = callback;
304
- }
305
- /**
306
- * Setup persistent broadcast listener for auth state changes
307
- * Idempotent - only sets up listener once
308
- */
309
- setupBroadcastListener() {
310
- if (this.broadcastListenerActive) return;
311
- this.broadcastListenerActive = true;
312
- chrome.runtime.onMessage.addListener((message) => {
313
- const msg = message;
314
- if ((msg == null ? void 0 : msg.type) === EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_BROADCAST) {
315
- if (msg.event === "authStateChanged") {
316
- this.handleAuthStateChangedBroadcast();
317
- }
318
- }
319
- return false;
320
- });
321
- this.logger.debug("Broadcast listener setup complete");
322
- }
323
- /**
324
- * Handle authStateChanged broadcast from background
325
- * Fetches fresh auth state and notifies via callback
326
- */
327
- async handleAuthStateChangedBroadcast() {
328
- this.logger.debug("Received authStateChanged broadcast, refreshing auth state");
329
- const authState = await this.getAuthState();
330
- this.setAuthState(authState);
331
- }
332
- /**
333
- * Generate a unique request ID for message correlation
334
- */
335
- generateRequestId() {
336
- return crypto.randomUUID();
337
- }
338
- /**
339
- * Get current client state
340
- */
341
- getState() {
342
- return this.state;
343
- }
344
- isClientInitialized() {
345
- return this.state.extension === "ready";
346
- }
347
- isServerReady() {
348
- return this.isClientInitialized() && this.state.server.status === "ready";
349
- }
350
- /**
351
- * Initialize extension discovery with optional timeout
352
- * Returns ExtensionState with extension and server status
353
- *
354
- * For sdk/ext:
355
- * - CAN store extensionId for fast restoration (via SET_EXTENSION_ID)
356
- * - Avoids chrome.runtime discovery messages on page reload
357
- */
358
- async init(params = {}) {
359
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
360
- if (!params.testConnection && !params.selectedConnection) {
361
- this.logger.info("No testConnection or selectedConnection, returning not-initialized state");
362
- const notInitState = bodhiJsCore.createExtensionStateNotInitialized();
363
- this.setState(notInitState);
364
- return notInitState;
365
- }
366
- if (this.extensionId && !params.testConnection) {
367
- this.logger.debug("Already initialized with extensionId, skipping discovery");
368
- return this.state;
369
- }
370
- const timeoutMs = params.timeoutMs ?? ((_b = (_a = this.config.initParams) == null ? void 0 : _a.extension) == null ? void 0 : _b.timeoutMs) ?? DISCOVERY_TIMEOUT_MS;
371
- const savedExtensionId = (_c = params.savedState) == null ? void 0 : _c.extensionId;
372
- try {
373
- if (!this.extensionId) {
374
- if (savedExtensionId) {
375
- this.logger.info("Restoring with known extensionId:", savedExtensionId);
376
- await this.sendExtMessageWithTimeout(
377
- EXT2EXT_CLIENT_ACTIONS.SET_EXTENSION_ID,
378
- { extensionId: savedExtensionId },
379
- timeoutMs
380
- );
381
- this.extensionId = savedExtensionId;
382
- } else {
383
- this.logger.info("Discovering bodhi-browser extension...");
384
- const discoveryParams = {
385
- attempts: (_e = (_d = this.config.initParams) == null ? void 0 : _d.extension) == null ? void 0 : _e.attempts,
386
- attemptWaitMs: (_g = (_f = this.config.initParams) == null ? void 0 : _f.extension) == null ? void 0 : _g.attemptWaitMs,
387
- attemptTimeout: (_i = (_h = this.config.initParams) == null ? void 0 : _h.extension) == null ? void 0 : _i.attemptTimeout
388
- };
389
- const body = await this.sendExtMessageWithTimeout(
390
- EXT2EXT_CLIENT_ACTIONS.DISCOVER_EXTENSION,
391
- discoveryParams,
392
- timeoutMs
393
- );
394
- this.extensionId = body.extensionId;
395
- this.logger.info("Extension discovered:", this.extensionId);
396
- }
397
- this.setupBroadcastListener();
398
- }
399
- const state = {
400
- type: "extension",
401
- extension: "ready",
402
- extensionId: this.extensionId,
403
- server: bodhiJsCore.PENDING_EXTENSION_READY
404
- };
405
- let serverState = bodhiJsCore.PENDING_EXTENSION_READY;
406
- if (params.testConnection) {
407
- try {
408
- serverState = await this.getServerState();
409
- this.logger.info("Server connectivity tested, state:", serverState.status);
410
- } catch (error) {
411
- this.logger.error("Failed to get server state:", error);
412
- serverState = bodhiJsCore.BACKEND_SERVER_NOT_REACHABLE;
413
- }
414
- }
415
- this.setState({ ...state, server: serverState });
416
- return this.state;
417
- } catch (error) {
418
- this.logger.error("Failed to initialize extension:", error);
419
- this.extensionId = null;
420
- const notFoundState = bodhiJsCore.createExtensionStateNotFound();
421
- this.setState(notFoundState);
422
- return this.state;
423
- }
424
- }
425
- /**
426
- * Helper method to send ext message with timeout support
427
- */
428
- async sendExtMessageWithTimeout(action, params, timeoutMs = 1e4) {
429
- const timeoutPromise = new Promise(
430
- (_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs)
431
- );
432
- return Promise.race([this.sendExtRequest(action, params), timeoutPromise]);
433
- }
434
- /**
435
- * Send an EXT2EXT_CLIENT_REQUEST message and await EXT2EXT_CLIENT_RESPONSE
436
- * Public for generic ext2ext testing
437
- */
438
- async sendExtRequest(action, params) {
439
- try {
440
- const requestId = this.generateRequestId();
441
- const response = await chrome.runtime.sendMessage({
442
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_REQUEST,
443
- requestId,
444
- request: {
445
- action,
446
- params
447
- }
448
- });
449
- if (!response) {
450
- throw bodhiJsCore.createOperationError("No response from background script", "extension_error");
451
- }
452
- if (response.type !== EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_RESPONSE) {
453
- throw bodhiJsCore.createOperationError(
454
- "Invalid response type from background script",
455
- "extension_error"
456
- );
457
- }
458
- const res = response.response;
459
- if (isExtError(res)) {
460
- const errorType = res.error.type || "extension_error";
461
- throw bodhiJsCore.createOperationError(res.error.message, errorType);
462
- }
463
- return res;
464
- } catch (err) {
465
- if (isOperationError(err)) {
466
- throw err;
467
- }
468
- throw bodhiJsCore.createOperationError(
469
- err instanceof Error ? err.message : "Unknown error occurred",
470
- "extension_error"
471
- );
472
- }
473
- }
474
- /**
475
- * Send an API_REQUEST message and await API_RESPONSE (internal)
476
- * Returns ext2ext-specific ExtClientApiResponseMessage
477
- */
478
- async sendRawApiMessage(method, endpoint, body, headers, authenticated) {
479
- const requestId = this.generateRequestId();
480
- const response = await chrome.runtime.sendMessage({
481
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_API_REQUEST,
482
- requestId,
483
- request: {
484
- method,
485
- endpoint,
486
- body,
487
- headers,
488
- authenticated
489
- }
490
- });
491
- return response;
492
- }
493
- /**
494
- * Send an API message and convert to protocol-agnostic ApiResponseResult
495
- */
496
- async sendApiRequest(method, endpoint, body, headers, authenticated) {
497
- const extResponse = await this.sendRawApiMessage(
498
- method,
499
- endpoint,
500
- body,
501
- headers,
502
- authenticated
503
- );
504
- if (isExtClientApiError(extResponse)) {
505
- const errorType = extResponse.error.type || "extension_error";
506
- return {
507
- error: {
508
- message: extResponse.error.message,
509
- type: errorType
510
- }
511
- };
512
- }
513
- return extResponse.response;
514
- }
515
- /**
516
- * Login user via OAuth
517
- * @throws ExtError if login fails
518
- * @returns AuthLoggedIn with login state and user info
519
- */
520
- async login() {
521
- return new Promise((resolve, reject) => {
522
- const listener = async (message) => {
523
- var _a;
524
- if (message && typeof message === "object" && "type" in message && message.type === "EXT2EXT_CLIENT_BROADCAST" && "event" in message && message.event === "authStateChanged") {
525
- chrome.runtime.onMessage.removeListener(listener);
526
- try {
527
- const authState = await this.getAuthState();
528
- if (bodhiJsCore.isAuthError(authState)) {
529
- reject(
530
- bodhiJsCore.createOperationError(`Login failed: ${(_a = authState.error) == null ? void 0 : _a.message}`, "auth-error")
531
- );
532
- return;
533
- }
534
- if (bodhiJsCore.isAuthLoggedOut(authState)) {
535
- reject(bodhiJsCore.createOperationError("Login failed: User is not logged in", "auth-error"));
536
- return;
537
- }
538
- this.setAuthState(authState);
539
- resolve(authState);
540
- } catch (err) {
541
- reject(err);
542
- }
543
- }
544
- };
545
- chrome.runtime.onMessage.addListener(listener);
546
- this.sendExtRequest(EXT2EXT_CLIENT_ACTIONS.LOGIN).catch((err) => {
547
- chrome.runtime.onMessage.removeListener(listener);
548
- reject(err);
549
- });
550
- });
551
- }
552
- /**
553
- * Logout current user
554
- * @throws ExtError if logout fails
555
- * @returns AuthLoggedOut with logged out state
556
- */
557
- async logout() {
558
- await this.sendExtRequest(EXT2EXT_CLIENT_ACTIONS.LOGOUT);
559
- const result = {
560
- isLoggedIn: false
561
- };
562
- this.setAuthState(result);
563
- return result;
564
- }
565
- /**
566
- * Get current authentication state
567
- * @returns AuthState (discriminated union: AuthLoggedIn | AuthLoggedOut)
568
- * @throws ExtError if request fails
569
- */
570
- async getAuthState() {
571
- if (!this.isClientInitialized()) {
572
- return bodhiJsCore.AUTH_EXT_NOT_INITIALIZED;
573
- }
574
- const body = await this.sendExtRequest(
575
- EXT2EXT_CLIENT_ACTIONS.GET_AUTH_STATE
576
- );
577
- return body.authState;
578
- }
579
- /**
580
- * Ping bodhi-browser-ext API via /ping endpoint
581
- */
582
- async pingApi() {
583
- return this.sendApiRequest("GET", "/ping");
584
- }
585
- /**
586
- * Fetch available models from /v1/models (authenticated)
587
- */
588
- async fetchModels() {
589
- return this.sendApiRequest(
590
- "GET",
591
- "/v1/models",
592
- void 0,
593
- void 0,
594
- true
595
- );
596
- }
597
- /**
598
- * Get backend server state
599
- * Calls /bodhi/v1/info and returns structured server state
600
- */
601
- async getServerState() {
602
- const result = await this.sendApiRequest("GET", "/bodhi/v1/info");
603
- if (bodhiJsCore.isApiResultOperationError(result)) {
604
- return {
605
- status: "not-reachable",
606
- error: result.error
607
- };
608
- }
609
- if (!bodhiJsCore.isApiResultSuccess(result)) {
610
- return {
611
- status: "not-reachable",
612
- error: { message: "API error from server", type: "extension_error" }
613
- };
614
- }
615
- const body = result.body;
616
- switch (body.status) {
617
- case "ready":
618
- return { status: "ready", version: body.version || "unknown" };
619
- case "setup":
620
- return {
621
- status: "setup",
622
- version: body.version || "unknown",
623
- error: body.error ? { message: body.error.message, type: body.error.type } : { message: "Setup required", type: "extension_error" }
624
- };
625
- case "resource-admin":
626
- return {
627
- status: "resource-admin",
628
- version: body.version || "unknown",
629
- error: body.error ? { message: body.error.message, type: body.error.type } : { message: "Resource admin required", type: "extension_error" }
630
- };
631
- case "error":
632
- return {
633
- status: "error",
634
- version: body.version || "unknown",
635
- error: body.error ? { message: body.error.message, type: body.error.type } : { message: "Server error", type: "extension_error" }
636
- };
637
- default:
638
- return {
639
- status: "not-reachable",
640
- error: { message: "Unknown server status", type: "extension_error" }
641
- };
642
- }
643
- }
644
- // ============================================================================
645
- // Streaming Methods
646
- // ============================================================================
647
- /**
648
- * Generic streaming method
649
- */
650
- async *stream(method, endpoint, body, headers, authenticated = true) {
651
- const requestId = this.generateRequestId();
652
- console.log("[ExtClient] Starting stream", {
653
- method,
654
- endpoint,
655
- requestId
656
- });
657
- const port = chrome.runtime.connect({ name: EXT2EXT_CLIENT_STREAM_PORT });
658
- const stream = new ReadableStream({
659
- start: (controller) => {
660
- port.onMessage.addListener((message) => {
661
- var _a, _b, _c;
662
- if (message.requestId !== requestId) return;
663
- switch (message.type) {
664
- case EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_DONE:
665
- console.log("[ExtClient] Stream complete", { requestId });
666
- controller.close();
667
- port.disconnect();
668
- break;
669
- case EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_ERROR:
670
- console.error("[ExtClient] Stream error", {
671
- requestId,
672
- error: JSON.stringify(message.error)
673
- });
674
- controller.error(
675
- bodhiJsCore.createOperationError(
676
- message.error.message,
677
- "extension_error"
678
- )
679
- );
680
- port.disconnect();
681
- break;
682
- case EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_API_ERROR: {
683
- const apiErr = message;
684
- console.error("[ExtClient] Stream API error", {
685
- requestId,
686
- error: (_a = apiErr.response.body) == null ? void 0 : _a.error
687
- });
688
- controller.error(
689
- bodhiJsCore.createApiError(
690
- ((_c = (_b = apiErr.response.body) == null ? void 0 : _b.error) == null ? void 0 : _c.message) || "API error",
691
- apiErr.response.status,
692
- apiErr.response.body
693
- )
694
- );
695
- port.disconnect();
696
- break;
697
- }
698
- case EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_CHUNK: {
699
- const chunkMsg = message;
700
- if (isApiSuccessResponse(chunkMsg.response)) {
701
- controller.enqueue(chunkMsg.response.body);
702
- }
703
- break;
704
- }
705
- }
706
- });
707
- port.onDisconnect.addListener(() => {
708
- console.log("[ExtClient] Port disconnected", { requestId });
709
- try {
710
- controller.error(
711
- bodhiJsCore.createOperationError("Connection closed unexpectedly", "extension_error")
712
- );
713
- } catch {
714
- }
715
- });
716
- port.postMessage({
717
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_REQUEST,
718
- requestId,
719
- request: { method, endpoint, body, headers, authenticated }
720
- });
721
- }
722
- });
723
- const reader = stream.getReader();
724
- try {
725
- while (true) {
726
- const { done, value } = await reader.read();
727
- if (done) {
728
- console.log("[ExtClient] Stream iteration complete");
729
- break;
730
- }
731
- yield value;
732
- }
733
- } finally {
734
- reader.releaseLock();
735
- }
736
- }
737
- /**
738
- * Chat streaming (uses generic stream internally)
739
- */
740
- async *streamChat(model, prompt, authenticated = true) {
741
- yield* this.stream(
742
- "POST",
743
- "/v1/chat/completions",
744
- {
745
- model,
746
- messages: [{ role: "user", content: prompt }],
747
- stream: true
748
- },
749
- void 0,
750
- authenticated
751
- );
752
- }
753
- /**
754
- * Serialize ext2ext client state for persistence
755
- */
756
- serialize() {
757
- return { extensionId: this.extensionId ?? void 0 };
758
- }
759
- /**
760
- * Debug dump of ExtClient internal state
761
- */
762
- async debug() {
763
- return {
764
- type: "ExtClient",
765
- state: this.state,
766
- authState: await this.getAuthState()
767
- };
768
- }
769
- }
770
- class ExtUIClient extends bodhiJsCore.BaseFacadeClient {
771
- constructor(authClientId, config, onStateChange, storagePrefix) {
772
- const normalizedConfig = {
773
- authServerUrl: config.authServerUrl || "https://id.getbodhi.app/realms/bodhi",
774
- userScope: config.userScope || "scope_user_user",
775
- logLevel: config.logLevel || "warn",
776
- initParams: config.initParams
777
- };
778
- super(authClientId, normalizedConfig, onStateChange, storagePrefix);
779
- }
780
- createLogger(config) {
781
- return new bodhiJsCore.Logger("ExtUIClient", config.logLevel);
782
- }
783
- createExtClient(config, onStateChange) {
784
- return new ExtClient(
785
- { logLevel: config.logLevel, initParams: config.initParams },
786
- onStateChange
787
- );
788
- }
789
- createDirectClient(authClientId, config, onStateChange) {
790
- return new DirectExtClient(
791
- {
792
- authClientId,
793
- authServerUrl: config.authServerUrl,
794
- userScope: config.userScope,
795
- logLevel: config.logLevel,
796
- storagePrefix: bodhiJsCore.STORAGE_PREFIXES.EXT
797
- },
798
- onStateChange
799
- );
800
- }
801
- }
802
- const DEV_EXTENSION_IDS = ["ggedphdcbekjlomjaidbajglgihbeaon"];
803
- const PROD_EXTENSION_IDS = ["bjdjhiombmfbcoeojijpfckljjghmjbf"];
804
- const _BodhiExtClient = class _BodhiExtClient {
805
- // ============================================================================
806
- // Constructor
807
- // ============================================================================
808
- constructor(authClientId, config) {
809
- this.isAuthenticating = false;
810
- this.state = "setup";
811
- this.listenersInitialized = false;
812
- this.refreshPromise = null;
813
- this.activeStreamPorts = /* @__PURE__ */ new Map();
814
- this.authClientId = authClientId;
815
- this.authServerUrl = (config == null ? void 0 : config.authServerUrl) || "https://id.getbodhi.app/realms/bodhi";
816
- this.userScope = (config == null ? void 0 : config.userScope) || "scope_user_user";
817
- this.extensionId = config == null ? void 0 : config.extensionId;
818
- this.logger = new bodhiJsCore.Logger("BodhiExtClient", (config == null ? void 0 : config.logLevel) || "warn");
819
- this.attempts = (config == null ? void 0 : config.attempts) ?? DISCOVERY_ATTEMPTS;
820
- this.attemptWaitMs = (config == null ? void 0 : config.attemptWaitMs) ?? DISCOVERY_ATTEMPT_WAIT_MS;
821
- this.attemptTimeout = (config == null ? void 0 : config.attemptTimeout) ?? DISCOVERY_ATTEMPT_TIMEOUT;
822
- this.authEndpoints = {
823
- authorize: `${this.authServerUrl}/protocol/openid-connect/auth`,
824
- token: `${this.authServerUrl}/protocol/openid-connect/token`,
825
- userinfo: `${this.authServerUrl}/protocol/openid-connect/userinfo`,
826
- logout: `${this.authServerUrl}/protocol/openid-connect/logout`,
827
- revoke: `${this.authServerUrl}/protocol/openid-connect/revoke`
828
- };
829
- if (this.extensionId) {
830
- this.logger.info(`[BodhiExtClient] Created client for extension: ${this.extensionId}`);
831
- } else {
832
- this.logger.info(
833
- `[BodhiExtClient] Created client without extension ID (call init() to discover)`
834
- );
835
- }
836
- }
837
- // ============================================================================
838
- // OAuth Utility Methods (Static)
839
- // ============================================================================
840
- static base64UrlEncode(buffer) {
841
- return btoa(String.fromCharCode(...new Uint8Array(buffer))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
842
- }
843
- static generateCodeVerifier() {
844
- const array = new Uint8Array(32);
845
- crypto.getRandomValues(array);
846
- return _BodhiExtClient.base64UrlEncode(array.buffer);
847
- }
848
- static async generateCodeChallenge(verifier) {
849
- const encoder = new TextEncoder();
850
- const data = encoder.encode(verifier);
851
- const digest = await crypto.subtle.digest("SHA-256", data);
852
- return _BodhiExtClient.base64UrlEncode(digest);
853
- }
854
- // ============================================================================
855
- // State Management
856
- // ============================================================================
857
- /**
858
- * Get current client state
859
- * @returns 'ready' if extension discovered, 'setup' otherwise
860
- */
861
- getState() {
862
- return this.state;
863
- }
864
- // ============================================================================
865
- // Extension Discovery (Private Methods)
866
- // ============================================================================
867
- /**
868
- * Get extension IDs for current environment
869
- */
870
- getExtensionIdsForEnvironment() {
871
- const isDev = true;
872
- const ids = isDev ? DEV_EXTENSION_IDS : PROD_EXTENSION_IDS;
873
- this.logger.info(`[Ext2Ext/Registry] Environment: ${"development"}`);
874
- this.logger.debug("[Ext2Ext/Registry] Using extension IDs:", ids);
875
- return ids;
876
- }
877
- /**
878
- * Ping extension using bodhi-browser-ext's EXT_REQUEST protocol
879
- */
880
- async pingExtension(extensionId) {
881
- this.logger.debug(
882
- `[Ext2Ext/Discovery] Pinging extension: ${extensionId} with timeout ${this.attemptTimeout}ms`
883
- );
884
- return new Promise((resolve, reject) => {
885
- const timer = setTimeout(() => {
886
- this.logger.debug(`[Ext2Ext/Discovery] Timeout waiting for extension ${extensionId}`);
887
- reject(new Error("Timeout"));
888
- }, this.attemptTimeout);
889
- try {
890
- const pingMessage = {
891
- type: MESSAGE_TYPES.EXT_REQUEST,
892
- requestId: crypto.randomUUID(),
893
- request: {
894
- action: "get_extension_id"
895
- }
896
- };
897
- this.logger.debug(`[Ext2Ext/Discovery] Sending message to ${extensionId}:`, pingMessage);
898
- chrome.runtime.sendMessage(extensionId, pingMessage, (response) => {
899
- clearTimeout(timer);
900
- if (chrome.runtime.lastError) {
901
- this.logger.error(
902
- `[Ext2Ext/Discovery] Error from extension ${extensionId}:`,
903
- chrome.runtime.lastError.message
904
- );
905
- reject(new Error(chrome.runtime.lastError.message));
906
- return;
907
- }
908
- this.logger.debug(`[Ext2Ext/Discovery] Response from ${extensionId}:`, response);
909
- const extResponse = response;
910
- if (extResponse && extResponse.type === MESSAGE_TYPES.EXT_RESPONSE) {
911
- this.logger.debug(`[Ext2Ext/Discovery] ✓ Extension ${extensionId} responded`);
912
- resolve(true);
913
- } else {
914
- this.logger.error(
915
- `[Ext2Ext/Discovery] Invalid response from ${extensionId}:`,
916
- response
917
- );
918
- reject(new Error("Invalid response"));
919
- }
920
- });
921
- } catch (error) {
922
- this.logger.error(`[Ext2Ext/Discovery] Exception pinging ${extensionId}:`, error);
923
- clearTimeout(timer);
924
- reject(error);
925
- }
926
- });
927
- }
928
- /**
929
- * Sleep helper for retry delays
930
- */
931
- sleep(ms) {
932
- return new Promise((resolve) => setTimeout(resolve, ms));
933
- }
934
- /**
935
- * Discover Bodhi extension sequentially through known IDs with retry logic
936
- * @param params Resolved discovery params
937
- */
938
- async discoverBodhiExtension(params) {
939
- const { attempts, attemptWaitMs: waitMs, attemptTimeout: timeout } = params;
940
- this.logger.info(
941
- `[Ext2Ext/Discovery] Starting discovery: ${attempts} attempts per ID, ${timeout}ms timeout, ${waitMs}ms between attempts`
942
- );
943
- const extensionIds = this.getExtensionIdsForEnvironment();
944
- this.logger.debug(
945
- `[Ext2Ext/Discovery] Will try ${extensionIds.length} extension(s):`,
946
- extensionIds
947
- );
948
- for (const extensionId of extensionIds) {
949
- for (let attempt = 1; attempt <= attempts; attempt++) {
950
- this.logger.debug(
951
- `[Ext2Ext/Discovery] Trying ${extensionId} - attempt ${attempt}/${attempts}`
952
- );
953
- try {
954
- await this.pingExtension(extensionId);
955
- this.logger.info(`[Ext2Ext/Discovery] ✓ Found: ${extensionId} on attempt ${attempt}`);
956
- return {
957
- success: true,
958
- extensionId
959
- };
960
- } catch (error) {
961
- this.logger.debug(
962
- `[Ext2Ext/Discovery] Attempt ${attempt} failed for ${extensionId}: ${error instanceof Error ? error.message : "Unknown error"}`
963
- );
964
- if (attempt < attempts) {
965
- await this.sleep(waitMs);
966
- }
967
- }
968
- }
969
- this.logger.warn(
970
- `[Ext2Ext/Discovery] ✗ Not found: ${extensionId} after ${attempts} attempts`
971
- );
972
- }
973
- const triedIds = extensionIds.join(", ");
974
- const errorMsg = `Extension not found. Tried ${extensionIds.length} IDs with ${attempts} attempts each: ${triedIds}`;
975
- this.logger.error(`[Ext2Ext/Discovery] ${errorMsg}`);
976
- return {
977
- success: false,
978
- error: errorMsg
979
- };
980
- }
981
- // ============================================================================
982
- // Message & Streaming Listeners (Private)
983
- // ============================================================================
984
- /**
985
- * Setup all listeners for UI connections (idempotent)
986
- */
987
- setupListeners() {
988
- if (this.listenersInitialized) {
989
- this.logger.debug("[BodhiExtClient] Listeners already initialized, skipping");
990
- return;
991
- }
992
- this.listenersInitialized = true;
993
- chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
994
- if (message.type !== EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_REQUEST && message.type !== EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_API_REQUEST) {
995
- return false;
996
- }
997
- if (this.state !== "ready") {
998
- const isDiscoverAction = message.type === EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_REQUEST && message.request.action === EXT2EXT_CLIENT_ACTIONS.DISCOVER_EXTENSION;
999
- if (!isDiscoverAction) {
1000
- if (message.type === EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_REQUEST) {
1001
- sendResponse({
1002
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_RESPONSE,
1003
- requestId: message.requestId,
1004
- response: {
1005
- error: {
1006
- message: this.createErrorClientNotInitialized(message),
1007
- type: "NOT_INITIALIZED"
1008
- }
1009
- }
1010
- });
1011
- } else {
1012
- sendResponse({
1013
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_API_RESPONSE,
1014
- requestId: message.requestId,
1015
- error: {
1016
- message: `Client not initialized. Extension discovery not complete, cannot handle type:${message.type}, message:${JSON.stringify(message)}`,
1017
- type: "NOT_INITIALIZED"
1018
- }
1019
- });
1020
- }
1021
- return true;
1022
- }
1023
- }
1024
- this.logger.debug(`[BodhiExtClient] Processing message.type=${message.type}`);
1025
- (async () => {
1026
- const response = await this.handleAction(message);
1027
- sendResponse(response);
1028
- })();
1029
- return true;
1030
- });
1031
- chrome.runtime.onConnect.addListener((port) => {
1032
- if (port.name !== EXT2EXT_CLIENT_STREAM_PORT) {
1033
- this.logger.debug("[BodhiExtClient] Ignoring port with name:", port.name);
1034
- return;
1035
- }
1036
- this.logger.info("[BodhiExtClient] Streaming port connected");
1037
- port.onMessage.addListener(async (message) => {
1038
- if (message.type !== EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_REQUEST) {
1039
- this.logger.warn("[BodhiExtClient] Unknown stream message type:", message.type);
1040
- port.postMessage({
1041
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_ERROR,
1042
- requestId: message.requestId,
1043
- error: {
1044
- message: "Unknown stream message type",
1045
- type: "extension_error"
1046
- }
1047
- });
1048
- return;
1049
- }
1050
- await this.handleStreamRequest(port, message);
1051
- });
1052
- port.onDisconnect.addListener(() => {
1053
- this.logger.info("[BodhiExtClient] Streaming port disconnected");
1054
- });
1055
- });
1056
- this.logger.info("[BodhiExtClient] Streaming listeners initialized");
1057
- }
1058
- /**
1059
- * Initialize client: setup listeners and discover bodhi-browser-ext
1060
- * @param params Resolved discovery params (already merged with constructor defaults)
1061
- * @throws Error if discovery fails
1062
- */
1063
- async init(params) {
1064
- this.setupListeners();
1065
- if (this.extensionId) {
1066
- this.state = "ready";
1067
- this.logger.warn(
1068
- `[BodhiExtClient] Already initialized with extension ID: ${this.extensionId}`
1069
- );
1070
- return;
1071
- }
1072
- this.logger.info("[BodhiExtClient] Starting discovery");
1073
- const resolvedParams = {
1074
- attempts: (params == null ? void 0 : params.attempts) ?? this.attempts,
1075
- attemptWaitMs: (params == null ? void 0 : params.attemptWaitMs) ?? this.attemptWaitMs,
1076
- attemptTimeout: (params == null ? void 0 : params.attemptTimeout) ?? this.attemptTimeout
1077
- };
1078
- const result = await this.discoverBodhiExtension(resolvedParams);
1079
- if (!result.success || !result.extensionId) {
1080
- throw new Error(result.error || "Discovery failed");
1081
- }
1082
- this.extensionId = result.extensionId;
1083
- this.state = "ready";
1084
- this.logger.info(`[BodhiExtClient] ✓ Initialized: ${this.extensionId}`);
1085
- }
1086
- /**
1087
- * Broadcast auth state change to all extension contexts
1088
- * @private
1089
- */
1090
- broadcastAuthStateChange() {
1091
- chrome.runtime.sendMessage({
1092
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_BROADCAST,
1093
- event: "authStateChanged"
1094
- }).catch((error) => {
1095
- this.logger.debug("[BodhiExtClient] No listeners for broadcast:", error.message);
1096
- });
1097
- }
1098
- /**
1099
- * Get extension ID from bodhi-browser-ext via EXT_REQUEST
1100
- * @returns Extension ID returned by bodhi-browser-ext
1101
- */
1102
- async getExtensionIdFromExt() {
1103
- this.logger.debug("[BodhiExtClient] Getting extension ID from bodhi-browser-ext");
1104
- const response = await this.sendExtRequest("get_extension_id");
1105
- this.logger.debug("[BodhiExtClient] Extension ID response:", response);
1106
- return response.extension_id;
1107
- }
1108
- /**
1109
- * Handle API request (EXT2EXT_CLIENT_API_REQUEST)
1110
- * Forwards to bodhi-browser-ext via sendApiRequest
1111
- * @param message API request message
1112
- * @returns API response message (success or error)
1113
- */
1114
- async handleApiRequest(message) {
1115
- const { requestId } = message;
1116
- this.logger.debug("[BodhiExtClient] Handling API request:", message.request);
1117
- try {
1118
- let requestHeaders = message.request.headers || {};
1119
- if (message.request.authenticated) {
1120
- const accessToken = await this._getAccessTokenRaw();
1121
- if (!accessToken) {
1122
- return {
1123
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_API_RESPONSE,
1124
- requestId,
1125
- error: {
1126
- message: "Not authenticated. Please log in first.",
1127
- type: "auth_error"
1128
- }
1129
- };
1130
- }
1131
- requestHeaders = {
1132
- ...requestHeaders,
1133
- Authorization: `Bearer ${accessToken}`
1134
- };
1135
- this.logger.debug("[BodhiExtClient] Injected auth token for authenticated request");
1136
- }
1137
- const apiResponse = await this.sendApiRequest(
1138
- message.request.method,
1139
- message.request.endpoint,
1140
- message.request.body,
1141
- requestHeaders
1142
- );
1143
- return {
1144
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_API_RESPONSE,
1145
- requestId,
1146
- response: apiResponse
1147
- };
1148
- } catch (error) {
1149
- this.logger.error("[BodhiExtClient] API request failed:", error);
1150
- return {
1151
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_API_RESPONSE,
1152
- requestId,
1153
- error: {
1154
- message: error instanceof Error ? error.message : "Unknown error",
1155
- type: "network_error"
1156
- }
1157
- };
1158
- }
1159
- }
1160
- /**
1161
- * Handle action-based request (EXT2EXT_CLIENT_REQUEST)
1162
- * Routes to ext2ext operations or local OAuth operations
1163
- * @param message Action request message
1164
- * @returns Action response message (success or error)
1165
- */
1166
- async handleExtClientRequest(message) {
1167
- const { requestId, request } = message;
1168
- const { action, params } = request;
1169
- this.logger.debug(`[BodhiExtClient] Handling action: ${action}`);
1170
- try {
1171
- let responseBody = {};
1172
- switch (action) {
1173
- case EXT2EXT_CLIENT_ACTIONS.DISCOVER_EXTENSION: {
1174
- const receivedParams = params;
1175
- await this.init(receivedParams);
1176
- const environment = "development";
1177
- this.logger.info("[BodhiExtClient] Discovery successful:", {
1178
- extensionId: this.extensionId,
1179
- environment
1180
- });
1181
- responseBody = {
1182
- extensionId: this.extensionId,
1183
- environment
1184
- };
1185
- break;
1186
- }
1187
- case EXT2EXT_CLIENT_ACTIONS.SET_EXTENSION_ID: {
1188
- const { extensionId } = params;
1189
- this.extensionId = extensionId;
1190
- this.state = "ready";
1191
- this.logger.info("[BodhiExtClient] Extension ID set:", { extensionId });
1192
- responseBody = { success: true };
1193
- break;
1194
- }
1195
- case EXT2EXT_CLIENT_ACTIONS.GET_EXTENSION_ID: {
1196
- const extResponse = await this.sendExtRequestRaw(EXT_ACTIONS.GET_EXTENSION_ID, params);
1197
- if (isExtError(extResponse.response)) {
1198
- return {
1199
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_RESPONSE,
1200
- requestId,
1201
- response: {
1202
- error: {
1203
- message: extResponse.response.error.message || `Extension request failed to get extension ID: ${JSON.stringify(extResponse.response)}`,
1204
- type: extResponse.response.error.type
1205
- }
1206
- }
1207
- };
1208
- }
1209
- return {
1210
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_RESPONSE,
1211
- requestId,
1212
- response: extResponse.response
1213
- };
1214
- }
1215
- case EXT2EXT_CLIENT_ACTIONS.LOGIN:
1216
- await this.login();
1217
- this.broadcastAuthStateChange();
1218
- break;
1219
- case EXT2EXT_CLIENT_ACTIONS.LOGOUT:
1220
- await this.logout();
1221
- this.broadcastAuthStateChange();
1222
- break;
1223
- case EXT2EXT_CLIENT_ACTIONS.GET_AUTH_STATE:
1224
- {
1225
- const authState = await this.getAuthState();
1226
- responseBody = { authState };
1227
- }
1228
- break;
1229
- default:
1230
- return {
1231
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_RESPONSE,
1232
- requestId,
1233
- response: {
1234
- error: { message: `Unknown action: ${action}`, type: "UNKNOWN_ACTION" }
1235
- }
1236
- };
1237
- }
1238
- return {
1239
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_RESPONSE,
1240
- requestId,
1241
- response: responseBody
1242
- };
1243
- } catch (error) {
1244
- this.logger.error("[BodhiExtClient] Unexpected error:", error);
1245
- return {
1246
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_RESPONSE,
1247
- requestId,
1248
- response: {
1249
- error: {
1250
- message: error instanceof Error ? error.message : `Unexpected error: ${JSON.stringify(error)}`
1251
- }
1252
- }
1253
- };
1254
- }
1255
- }
1256
- // Implementation signature (must be compatible with all overloads)
1257
- async handleAction(message) {
1258
- switch (message.type) {
1259
- case EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_API_REQUEST:
1260
- return this.handleApiRequest(message);
1261
- case EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_REQUEST:
1262
- return this.handleExtClientRequest(message);
1263
- default: {
1264
- const { requestId } = message;
1265
- this.logger.error("[BodhiExtClient] Unknown message type:", message.type);
1266
- return {
1267
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_RESPONSE,
1268
- requestId,
1269
- response: {
1270
- error: {
1271
- message: `Unknown message type: ${message.type}`,
1272
- type: "UNKNOWN_MESSAGE_TYPE"
1273
- }
1274
- }
1275
- };
1276
- }
1277
- }
1278
- }
1279
- // ============================================================================
1280
- // OAuth Public Methods
1281
- // ============================================================================
1282
- /**
1283
- * Login user via OAuth2 + PKCE flow
1284
- * @throws Error if login fails
1285
- */
1286
- async login() {
1287
- if (this.isAuthenticating) {
1288
- return;
1289
- }
1290
- const authState = await this.getAuthState();
1291
- if (authState.isLoggedIn) {
1292
- return;
1293
- }
1294
- this.isAuthenticating = true;
1295
- try {
1296
- if (!this.extensionId) {
1297
- throw new Error("Extension not discovered. Please detect Bodhi extension before login.");
1298
- }
1299
- const resourceScope = await this.requestAccess();
1300
- const fullScope = `openid profile email roles ${this.userScope} ${resourceScope}`;
1301
- const codeVerifier = _BodhiExtClient.generateCodeVerifier();
1302
- const codeChallenge = await _BodhiExtClient.generateCodeChallenge(codeVerifier);
1303
- const state = _BodhiExtClient.generateCodeVerifier();
1304
- await chrome.storage.session.set({
1305
- codeVerifier,
1306
- state,
1307
- authInProgress: true
1308
- });
1309
- const redirectUri = chrome.identity.getRedirectURL("callback");
1310
- const authUrl = new URL(this.authEndpoints.authorize);
1311
- authUrl.searchParams.set("client_id", this.authClientId);
1312
- authUrl.searchParams.set("response_type", "code");
1313
- authUrl.searchParams.set("redirect_uri", redirectUri);
1314
- authUrl.searchParams.set("scope", fullScope);
1315
- authUrl.searchParams.set("code_challenge", codeChallenge);
1316
- authUrl.searchParams.set("code_challenge_method", "S256");
1317
- authUrl.searchParams.set("state", state);
1318
- return new Promise((resolve, reject) => {
1319
- chrome.identity.launchWebAuthFlow(
1320
- {
1321
- url: authUrl.toString(),
1322
- interactive: true
1323
- },
1324
- async (redirectUrl) => {
1325
- await chrome.storage.session.set({ authInProgress: false });
1326
- if (chrome.runtime.lastError) {
1327
- await chrome.storage.session.remove(["codeVerifier", "state"]);
1328
- reject(chrome.runtime.lastError);
1329
- return;
1330
- }
1331
- if (!redirectUrl) {
1332
- await chrome.storage.session.remove(["codeVerifier", "state"]);
1333
- reject(new Error("No redirect URL received"));
1334
- return;
1335
- }
1336
- try {
1337
- const url = new URL(redirectUrl);
1338
- const code = url.searchParams.get("code");
1339
- const returnedState = url.searchParams.get("state");
1340
- const { state: savedState } = await chrome.storage.session.get("state");
1341
- if (returnedState !== savedState) {
1342
- await chrome.storage.session.remove(["codeVerifier", "state"]);
1343
- reject(new Error("State mismatch - possible CSRF"));
1344
- return;
1345
- }
1346
- if (!code) {
1347
- await chrome.storage.session.remove(["codeVerifier", "state"]);
1348
- reject(new Error("No authorization code received"));
1349
- return;
1350
- }
1351
- await this.exchangeCodeForTokens(code);
1352
- await chrome.storage.session.remove(["codeVerifier", "state"]);
1353
- resolve();
1354
- } catch (error) {
1355
- await chrome.storage.session.remove(["codeVerifier", "state"]);
1356
- reject(error);
1357
- }
1358
- }
1359
- );
1360
- });
1361
- } finally {
1362
- this.isAuthenticating = false;
1363
- }
1364
- }
1365
- /**
1366
- * Exchange authorization code for tokens (private helper for login)
1367
- * @param code Authorization code from OAuth callback
1368
- */
1369
- async exchangeCodeForTokens(code) {
1370
- const authState = await this.getAuthState();
1371
- if (authState.isLoggedIn) {
1372
- return;
1373
- }
1374
- const { codeVerifier } = await chrome.storage.session.get("codeVerifier");
1375
- const redirectUri = chrome.identity.getRedirectURL("callback");
1376
- const response = await fetch(this.authEndpoints.token, {
1377
- method: "POST",
1378
- headers: {
1379
- "Content-Type": "application/x-www-form-urlencoded"
1380
- },
1381
- body: new URLSearchParams({
1382
- grant_type: "authorization_code",
1383
- code,
1384
- redirect_uri: redirectUri,
1385
- client_id: this.authClientId,
1386
- code_verifier: codeVerifier
1387
- })
1388
- });
1389
- if (!response.ok) {
1390
- const errorText = await response.text();
1391
- throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
1392
- }
1393
- const tokens = await response.json();
1394
- await this.storeTokens({
1395
- accessToken: tokens.access_token,
1396
- refreshToken: tokens.refresh_token,
1397
- idToken: tokens.id_token,
1398
- expiresIn: tokens.expires_in
1399
- });
1400
- }
1401
- /**
1402
- * Get current authentication state
1403
- * @returns AuthState (discriminated union: AuthLoggedIn | AuthLoggedOut)
1404
- */
1405
- async getAuthState() {
1406
- const accessToken = await this._getAccessTokenRaw();
1407
- if (!accessToken) {
1408
- return { isLoggedIn: false };
1409
- }
1410
- try {
1411
- const claims = this.parseJwt(accessToken);
1412
- const userInfo = {
1413
- sub: claims.sub,
1414
- email: claims.email,
1415
- name: claims.name,
1416
- given_name: claims.given_name,
1417
- family_name: claims.family_name,
1418
- preferred_username: claims.preferred_username
1419
- };
1420
- return {
1421
- isLoggedIn: true,
1422
- userInfo,
1423
- accessToken
1424
- };
1425
- } catch (error) {
1426
- this.logger.error("Failed to parse token:", error);
1427
- return { isLoggedIn: false };
1428
- }
1429
- }
1430
- /**
1431
- * Logout current user and revoke tokens
1432
- */
1433
- async logout() {
1434
- const { refreshToken } = await chrome.storage.session.get("refreshToken");
1435
- if (refreshToken) {
1436
- try {
1437
- await fetch(this.authEndpoints.revoke, {
1438
- method: "POST",
1439
- headers: {
1440
- "Content-Type": "application/x-www-form-urlencoded"
1441
- },
1442
- body: new URLSearchParams({
1443
- token: refreshToken,
1444
- client_id: this.authClientId,
1445
- token_type_hint: "refresh_token"
1446
- })
1447
- });
1448
- } catch (error) {
1449
- this.logger.warn("[OAuth] Token revocation failed:", error);
1450
- }
1451
- }
1452
- await this.clearTokens();
1453
- }
1454
- // ============================================================================
1455
- // Ext2Ext Communication (Private Methods)
1456
- // ============================================================================
1457
- /**
1458
- * Send EXT_REQUEST message to bodhi-browser-ext
1459
- * @param action The action to perform
1460
- * @param params Optional parameters
1461
- * @returns Response body (unwrapped, throws on error)
1462
- */
1463
- async sendExtRequest(action, params) {
1464
- const response = await this.sendExtRequestRaw(action, params);
1465
- if (isExtError(response.response)) {
1466
- this.logger.error("[BodhiExtClient] Extension error:", response.response.error);
1467
- throw new Error(
1468
- response.response.error.message || `Extension request failed: ${JSON.stringify(response.response)}`
1469
- );
1470
- }
1471
- return response.response;
1472
- }
1473
- /**
1474
- * Send API_REQUEST message to bodhi-browser-ext for HTTP operations
1475
- * @param method HTTP method (GET, POST, etc.)
1476
- * @param endpoint API endpoint path
1477
- * @param body Optional request body
1478
- * @param headers Optional headers
1479
- * @returns API response from LLM server via bodhi-browser-ext
1480
- */
1481
- async sendApiRequest(method, endpoint, body, headers) {
1482
- if (!this.extensionId) {
1483
- throw new Error(this.createErrorClientNotInitialized({ type: "api", method, endpoint }));
1484
- }
1485
- this.logger.debug(
1486
- `[BodhiExtClient] Sending API_REQUEST: method=${method}, endpoint=${endpoint}`,
1487
- body ? { body } : ""
1488
- );
1489
- const requestId = crypto.randomUUID();
1490
- const message = {
1491
- type: MESSAGE_TYPES.API_REQUEST,
1492
- requestId,
1493
- request: {
1494
- method,
1495
- endpoint,
1496
- body,
1497
- headers
1498
- }
1499
- };
1500
- this.logger.debug(`[BodhiExtClient] Request ID: ${requestId}, Extension: ${this.extensionId}`);
1501
- return new Promise((resolve, reject) => {
1502
- try {
1503
- chrome.runtime.sendMessage(this.extensionId, message, (response) => {
1504
- if (chrome.runtime.lastError) {
1505
- this.logger.error(
1506
- `[BodhiExtClient] Chrome runtime error for request ${requestId}:`,
1507
- chrome.runtime.lastError
1508
- );
1509
- reject(new Error(chrome.runtime.lastError.message));
1510
- return;
1511
- }
1512
- this.logger.debug(`[BodhiExtClient] Response for request ${requestId}:`, response);
1513
- if (!response) {
1514
- this.logger.error(`[BodhiExtClient] No response received for request ${requestId}`);
1515
- reject(new Error("No response from extension"));
1516
- return;
1517
- }
1518
- if (response.type === MESSAGE_TYPES.API_RESPONSE && response.requestId === requestId) {
1519
- if ("error" in response) {
1520
- this.logger.error(`[BodhiExtClient] API error for ${requestId}:`, response.error);
1521
- reject(new Error(response.error.message));
1522
- } else {
1523
- this.logger.debug(`[BodhiExtClient] ✓ Valid API_RESPONSE for ${requestId}`);
1524
- resolve(response.response);
1525
- }
1526
- } else {
1527
- this.logger.error(
1528
- `[BodhiExtClient] Invalid response format for ${requestId}:`,
1529
- response
1530
- );
1531
- reject(new Error("Invalid response format"));
1532
- }
1533
- });
1534
- } catch (error) {
1535
- this.logger.error(`[BodhiExtClient] Exception sending message for ${requestId}:`, error);
1536
- reject(error);
1537
- }
1538
- });
1539
- }
1540
- /**
1541
- * Send EXT_REQUEST message to bodhi-browser-ext and return full response
1542
- * @param action The action to perform
1543
- * @param params Optional parameters
1544
- * @returns Full ExtResponseMessage from bodhi-browser-ext
1545
- */
1546
- async sendExtRequestRaw(action, params) {
1547
- if (!this.extensionId) {
1548
- throw new Error(this.createErrorClientNotInitialized({ type: "ext", action, params }));
1549
- }
1550
- this.logger.debug(
1551
- `[BodhiExtClient] Sending EXT_REQUEST (raw): action=${action}`,
1552
- params ? { params } : ""
1553
- );
1554
- const requestId = crypto.randomUUID();
1555
- const message = {
1556
- type: MESSAGE_TYPES.EXT_REQUEST,
1557
- requestId,
1558
- request: {
1559
- action,
1560
- params
1561
- }
1562
- };
1563
- this.logger.debug(`[BodhiExtClient] Request ID: ${requestId}, Extension: ${this.extensionId}`);
1564
- return new Promise((resolve, reject) => {
1565
- try {
1566
- chrome.runtime.sendMessage(this.extensionId, message, (response) => {
1567
- if (chrome.runtime.lastError) {
1568
- this.logger.error(
1569
- `[BodhiExtClient] Chrome runtime error for request ${requestId}:`,
1570
- chrome.runtime.lastError
1571
- );
1572
- reject(new Error(chrome.runtime.lastError.message));
1573
- return;
1574
- }
1575
- this.logger.debug(`[BodhiExtClient] Response for request ${requestId}:`, response);
1576
- if (!response) {
1577
- this.logger.error(`[BodhiExtClient] No response received for request ${requestId}`);
1578
- reject(new Error("No response from extension"));
1579
- return;
1580
- }
1581
- if (response.type === MESSAGE_TYPES.EXT_RESPONSE && response.requestId === requestId) {
1582
- this.logger.debug(`[BodhiExtClient] ✓ Valid EXT_RESPONSE for ${requestId}`);
1583
- resolve(response);
1584
- } else {
1585
- this.logger.error(
1586
- `[BodhiExtClient] Invalid response format for ${requestId}:`,
1587
- response
1588
- );
1589
- reject(new Error("Invalid response format"));
1590
- }
1591
- });
1592
- } catch (error) {
1593
- this.logger.error(`[BodhiExtClient] Exception sending message for ${requestId}:`, error);
1594
- reject(error);
1595
- }
1596
- });
1597
- }
1598
- /**
1599
- * Handle streaming request from UI port
1600
- * Connects to bodhi-browser-ext and forwards chunks directly to UI port
1601
- * @param uiPort Port connected from UI
1602
- * @param message Stream request message from UI
1603
- */
1604
- async handleStreamRequest(uiPort, message) {
1605
- const { requestId, request } = message;
1606
- const { method, endpoint, body, headers, authenticated } = request;
1607
- this.logger.debug("[BodhiExtClient] Processing stream request:", {
1608
- requestId,
1609
- method,
1610
- endpoint,
1611
- authenticated
1612
- });
1613
- if (!this.extensionId) {
1614
- uiPort.postMessage({
1615
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_ERROR,
1616
- requestId,
1617
- error: {
1618
- message: this.createErrorClientNotInitialized(message),
1619
- type: "extension_error"
1620
- }
1621
- });
1622
- }
1623
- try {
1624
- let requestHeaders = { ...headers };
1625
- if (authenticated !== false) {
1626
- const accessToken = await this._getAccessTokenRaw();
1627
- if (!accessToken) {
1628
- uiPort.postMessage({
1629
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_ERROR,
1630
- requestId,
1631
- error: {
1632
- message: "Not authenticated. Please log in first.",
1633
- type: "extension_error"
1634
- }
1635
- });
1636
- return;
1637
- }
1638
- requestHeaders = {
1639
- ...requestHeaders,
1640
- Authorization: `Bearer ${accessToken}`
1641
- };
1642
- this.logger.debug("[BodhiExtClient] Injected auth token for authenticated request");
1643
- }
1644
- const bodhiPort = chrome.runtime.connect(this.extensionId, {
1645
- name: BODHI_STREAM_PORT
1646
- });
1647
- this.activeStreamPorts.set(requestId, bodhiPort);
1648
- const timeoutId = setTimeout(() => {
1649
- if (this.activeStreamPorts.has(requestId)) {
1650
- this.logger.error(`[BodhiExtClient] Stream timeout for ${requestId}`);
1651
- uiPort.postMessage({
1652
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_ERROR,
1653
- requestId,
1654
- error: {
1655
- message: "Stream request timed out",
1656
- type: "timeout_error"
1657
- }
1658
- });
1659
- this.cleanupStreamPort(requestId);
1660
- }
1661
- }, _BodhiExtClient.STREAM_TIMEOUT);
1662
- bodhiPort.onMessage.addListener((streamMessage) => {
1663
- if (isStreamChunk(streamMessage)) {
1664
- const response = streamMessage.response;
1665
- const responseBody = response.body;
1666
- if (response.status >= 400) {
1667
- uiPort.postMessage({
1668
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_API_ERROR,
1669
- requestId,
1670
- response
1671
- });
1672
- } else if (responseBody == null ? void 0 : responseBody.done) {
1673
- uiPort.postMessage({
1674
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_DONE,
1675
- requestId
1676
- });
1677
- this.logger.info(`[BodhiExtClient] Stream complete for ${requestId}`);
1678
- clearTimeout(timeoutId);
1679
- this.cleanupStreamPort(requestId);
1680
- } else {
1681
- uiPort.postMessage({
1682
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_CHUNK,
1683
- requestId,
1684
- response
1685
- });
1686
- }
1687
- } else if (isStreamApiError(streamMessage)) {
1688
- this.logger.error(
1689
- `[BodhiExtClient] Stream API error for ${requestId}: ${streamMessage.response.status}`
1690
- );
1691
- uiPort.postMessage({
1692
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_API_ERROR,
1693
- requestId,
1694
- response: streamMessage.response
1695
- });
1696
- } else if (isStreamError(streamMessage)) {
1697
- this.logger.error(
1698
- `[BodhiExtClient] Stream error for ${requestId}:`,
1699
- streamMessage.error.message
1700
- );
1701
- uiPort.postMessage({
1702
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_ERROR,
1703
- requestId,
1704
- error: {
1705
- message: `stream error: ${JSON.stringify(streamMessage)}`,
1706
- type: "extension_error"
1707
- }
1708
- });
1709
- clearTimeout(timeoutId);
1710
- this.cleanupStreamPort(requestId);
1711
- }
1712
- });
1713
- bodhiPort.onDisconnect.addListener(() => {
1714
- clearTimeout(timeoutId);
1715
- if (this.activeStreamPorts.has(requestId)) {
1716
- this.logger.error(`[BodhiExtClient] Bodhi port disconnected for ${requestId}`);
1717
- uiPort.postMessage({
1718
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_ERROR,
1719
- requestId,
1720
- error: {
1721
- message: "Connection to Bodhi extension closed unexpectedly",
1722
- type: "network_error"
1723
- }
1724
- });
1725
- this.activeStreamPorts.delete(requestId);
1726
- }
1727
- });
1728
- const streamRequest = {
1729
- type: MESSAGE_TYPES.STREAM_REQUEST,
1730
- requestId,
1731
- request: {
1732
- method,
1733
- endpoint,
1734
- body,
1735
- headers: requestHeaders
1736
- }
1737
- };
1738
- this.logger.debug(`[BodhiExtClient] Sending stream request to bodhi port:`, streamRequest);
1739
- bodhiPort.postMessage(streamRequest);
1740
- } catch (error) {
1741
- const err = error;
1742
- this.logger.error("[BodhiExtClient] Stream error:", JSON.stringify(err.message));
1743
- uiPort.postMessage({
1744
- type: EXT2EXT_CLIENT_MESSAGE_TYPES.EXT2EXT_CLIENT_STREAM_ERROR,
1745
- requestId,
1746
- error: {
1747
- message: `uncaught error: ${JSON.stringify({ error: err, message: err.message })}`,
1748
- type: "extension_error"
1749
- }
1750
- });
1751
- }
1752
- }
1753
- /**
1754
- * Clean up a streaming port connection
1755
- */
1756
- cleanupStreamPort(requestId) {
1757
- const port = this.activeStreamPorts.get(requestId);
1758
- if (port) {
1759
- try {
1760
- port.disconnect();
1761
- } catch {
1762
- }
1763
- this.activeStreamPorts.delete(requestId);
1764
- }
1765
- }
1766
- // ============================================================================
1767
- // Resource Access (Private Methods)
1768
- // ============================================================================
1769
- /**
1770
- * Request resource access scope from backend via bodhi-browser-ext.
1771
- * Required for authenticated API access - token will include aud claim.
1772
- * @returns The resource scope string (e.g., "scope_resource-xxx")
1773
- * @throws Error if request fails or returns empty scope
1774
- */
1775
- async requestAccess() {
1776
- const response = await this.sendApiRequest(
1777
- "POST",
1778
- "/bodhi/v1/apps/request-access",
1779
- { app_client_id: this.authClientId }
1780
- );
1781
- if (!isApiSuccessResponse(response)) {
1782
- this.logger.error("[BodhiExtClient] Failed to get resource access scope: API error");
1783
- throw new Error("Failed to get resource access scope: API error");
1784
- }
1785
- return response.body.scope;
1786
- }
1787
- // ============================================================================
1788
- // Token Management (Private Methods)
1789
- // ============================================================================
1790
- async storeTokens(tokens) {
1791
- const expiresAt = Date.now() + (tokens.expiresIn || 3600) * 1e3;
1792
- await chrome.storage.session.set({
1793
- accessToken: tokens.accessToken,
1794
- refreshToken: tokens.refreshToken,
1795
- idToken: tokens.idToken,
1796
- expiresAt
1797
- });
1798
- }
1799
- async _getAccessTokenRaw() {
1800
- const { accessToken, expiresAt } = await chrome.storage.session.get([
1801
- "accessToken",
1802
- "expiresAt"
1803
- ]);
1804
- if (!accessToken || !expiresAt) {
1805
- return null;
1806
- }
1807
- if (Date.now() >= expiresAt - 5 * 1e3) {
1808
- const { refreshToken } = await chrome.storage.session.get("refreshToken");
1809
- if (refreshToken) {
1810
- return this._tryRefreshToken(refreshToken);
1811
- }
1812
- return null;
1813
- }
1814
- return accessToken;
1815
- }
1816
- /**
1817
- * Try to refresh access token using refresh token
1818
- * Race condition prevention: Returns existing promise if refresh already in progress
1819
- */
1820
- async _tryRefreshToken(refreshToken) {
1821
- if (this.refreshPromise) {
1822
- this.logger.debug("Refresh already in progress, returning existing promise");
1823
- return this.refreshPromise;
1824
- }
1825
- this.refreshPromise = this._doRefreshToken(refreshToken);
1826
- try {
1827
- return await this.refreshPromise;
1828
- } finally {
1829
- this.refreshPromise = null;
1830
- }
1831
- }
1832
- /**
1833
- * Perform the actual token refresh
1834
- */
1835
- async _doRefreshToken(refreshToken) {
1836
- this.logger.debug("Refreshing access token");
1837
- try {
1838
- const tokens = await bodhiJsCore.refreshAccessToken(
1839
- this.authEndpoints.token,
1840
- refreshToken,
1841
- this.authClientId
1842
- );
1843
- if (tokens) {
1844
- await this._storeRefreshedTokens(tokens);
1845
- this.logger.info("Token refreshed successfully");
1846
- this.broadcastAuthStateChange();
1847
- return tokens.access_token;
1848
- }
1849
- } catch (error) {
1850
- this.logger.warn("Token refresh failed:", error);
1851
- }
1852
- this.logger.warn("Token refresh failed, keeping tokens for manual retry");
1853
- throw bodhiJsCore.createOperationError(
1854
- "Access token expired and unable to refresh. Try logging out and logging in again.",
1855
- "token_refresh_failed"
1856
- );
1857
- }
1858
- /**
1859
- * Store refreshed tokens
1860
- */
1861
- async _storeRefreshedTokens(tokens) {
1862
- const expiresAt = Date.now() + tokens.expires_in * 1e3;
1863
- const storageData = {
1864
- accessToken: tokens.access_token,
1865
- expiresAt
1866
- };
1867
- if (tokens.refresh_token) {
1868
- storageData.refreshToken = tokens.refresh_token;
1869
- }
1870
- if (tokens.id_token) {
1871
- storageData.idToken = tokens.id_token;
1872
- }
1873
- await chrome.storage.session.set(storageData);
1874
- }
1875
- async clearTokens() {
1876
- await chrome.storage.session.remove([
1877
- "accessToken",
1878
- "refreshToken",
1879
- "idToken",
1880
- "expiresAt",
1881
- "codeVerifier",
1882
- "state",
1883
- "authInProgress",
1884
- "bodhiUserInfo"
1885
- ]);
1886
- }
1887
- parseJwt(token) {
1888
- const base64Url = token.split(".")[1];
1889
- const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
1890
- const jsonPayload = decodeURIComponent(
1891
- atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
1892
- );
1893
- return JSON.parse(jsonPayload);
1894
- }
1895
- createErrorClientNotInitialized(message) {
1896
- return `Client not initialized. Extension discovery not triggered nor extensionId set, cannot handle request: ${JSON.stringify(message)}`;
1897
- }
1898
- };
1899
- _BodhiExtClient.STREAM_TIMEOUT = 12e4;
1900
- let BodhiExtClient = _BodhiExtClient;
1901
- exports.BodhiExtClient = BodhiExtClient;
1902
- exports.DISCOVERY_ATTEMPTS = DISCOVERY_ATTEMPTS;
1903
- exports.DISCOVERY_ATTEMPT_TIMEOUT = DISCOVERY_ATTEMPT_TIMEOUT;
1904
- exports.DISCOVERY_ATTEMPT_WAIT_MS = DISCOVERY_ATTEMPT_WAIT_MS;
1905
- exports.DISCOVERY_TIMEOUT_MS = DISCOVERY_TIMEOUT_MS;
1906
- exports.EXT2EXT_CLIENT_ACTIONS = EXT2EXT_CLIENT_ACTIONS;
1907
- exports.EXT2EXT_CLIENT_MESSAGE_TYPES = EXT2EXT_CLIENT_MESSAGE_TYPES;
1908
- exports.EXT2EXT_CLIENT_STREAM_PORT = EXT2EXT_CLIENT_STREAM_PORT;
1909
- exports.ExtUIClient = ExtUIClient;
1910
- exports.isExtClientApiError = isExtClientApiError;
1911
- //# sourceMappingURL=bodhi-ext.cjs.js.map
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=require("@bodhiapp/bodhi-js-core"),c={EXT2EXT_CLIENT_REQUEST:"EXT2EXT_CLIENT_REQUEST",EXT2EXT_CLIENT_RESPONSE:"EXT2EXT_CLIENT_RESPONSE",EXT2EXT_CLIENT_BROADCAST:"EXT2EXT_CLIENT_BROADCAST",EXT2EXT_CLIENT_API_REQUEST:"EXT2EXT_CLIENT_API_REQUEST",EXT2EXT_CLIENT_API_RESPONSE:"EXT2EXT_CLIENT_API_RESPONSE",EXT2EXT_CLIENT_STREAM_REQUEST:"EXT2EXT_CLIENT_STREAM_REQUEST",EXT2EXT_CLIENT_STREAM_CHUNK:"EXT2EXT_CLIENT_STREAM_CHUNK",EXT2EXT_CLIENT_STREAM_ERROR:"EXT2EXT_CLIENT_STREAM_ERROR",EXT2EXT_CLIENT_STREAM_API_ERROR:"EXT2EXT_CLIENT_STREAM_API_ERROR",EXT2EXT_CLIENT_STREAM_DONE:"EXT2EXT_CLIENT_STREAM_DONE"},x="ext2ext-client-stream",S={LOGIN:"login",LOGOUT:"logout",GET_AUTH_STATE:"getAuthState",DISCOVER_EXTENSION:"discoverBodhiExtension",GET_EXTENSION_ID:"get_extension_id",SET_EXTENSION_ID:"setExtensionId"},w=5e3,N=3,O=500,X=500;function P(a){return"error"in a}class L extends h.DirectClientBase{constructor(e,t){super({...e,storagePrefix:h.STORAGE_PREFIXES.EXT},"DirectExtClient",t)}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}`,i=h.generateCodeVerifier(),s=await h.generateCodeChallenge(i),o=h.generateCodeVerifier();await chrome.storage.session.set({[this.storageKeys.CODE_VERIFIER]:i,[this.storageKeys.STATE]:o});const n=chrome.identity.getRedirectURL("callback"),E=new URL(this.authEndpoints.authorize);return E.searchParams.set("client_id",this.authClientId),E.searchParams.set("response_type","code"),E.searchParams.set("redirect_uri",n),E.searchParams.set("scope",r),E.searchParams.set("code_challenge",s),E.searchParams.set("code_challenge_method","S256"),E.searchParams.set("state",o),new Promise((d,u)=>{chrome.identity.launchWebAuthFlow({url:E.toString(),interactive:!0},async l=>{if(chrome.runtime.lastError){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(chrome.runtime.lastError);return}if(!l){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(h.createOperationError("No redirect URL received","oauth-error"));return}try{const T=new URL(l),p=T.searchParams.get("code"),g=T.searchParams.get("state"),R=(await chrome.storage.session.get(this.storageKeys.STATE))[this.storageKeys.STATE];if(g!==R){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(h.createOperationError("State mismatch - possible CSRF","oauth-error"));return}if(!p){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(h.createOperationError("No authorization code received","oauth-error"));return}await this.exchangeCodeForTokens(p);const C=await this.getAuthState();if(!C.isLoggedIn)throw h.createOperationError("Login failed","oauth-error");const A=C;this.setAuthState(A),await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),d(A)}catch(T){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(T)}})})}async logout(){const t=(await chrome.storage.session.get(this.storageKeys.REFRESH_TOKEN))[this.storageKeys.REFRESH_TOKEN];if(t)try{const i=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:i})}catch(i){this.logger.warn("Token revocation failed:",i)}await chrome.storage.session.remove([this.storageKeys.ACCESS_TOKEN,this.storageKeys.REFRESH_TOKEN,this.storageKeys.EXPIRES_AT,this.storageKeys.RESOURCE_SCOPE]);const r={isLoggedIn:!1};return this.setAuthState(r),r}async requestResourceAccess(){const e=await this.sendApiRequest("POST","/bodhi/v1/apps/request-access",{app_client_id:this.authClientId},{},!1);if(h.isApiResultOperationError(e))throw new Error("Failed to get resource access scope from server");if(!h.isApiResultSuccess(e))throw new Error("Failed to get resource access scope from server: API error");const t=e.body.scope;return await chrome.storage.session.set({[this.storageKeys.RESOURCE_SCOPE]:t}),t}async exchangeCodeForTokens(e){const r=(await chrome.storage.session.get(this.storageKeys.CODE_VERIFIER))[this.storageKeys.CODE_VERIFIER],i=chrome.identity.getRedirectURL("callback"),s=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:i,client_id:this.authClientId,code_verifier:r})});if(!s.ok){const E=await s.text();throw new Error(`Token exchange failed: ${s.status} ${E}`)}const o=await s.json(),n=Date.now()+(o.expires_in||3600)*1e3;await chrome.storage.session.set({[this.storageKeys.ACCESS_TOKEN]:o.access_token,[this.storageKeys.REFRESH_TOKEN]:o.refresh_token,[this.storageKeys.EXPIRES_AT]:n}),await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE])}async _storageGet(e){const r=(await chrome.storage.session.get(e))[e];return r!==void 0?String(r):null}async _storageSet(e){await chrome.storage.session.set(e)}async _storageRemove(e){await chrome.storage.session.remove(e)}_getRedirectUri(){return chrome.identity.getRedirectURL("callback")}}function b(a){return a!==null&&typeof a=="object"}function k(a){return b(a)&&"message"in a&&typeof a.message=="string"&&"type"in a&&typeof a.type=="string"}const m={API_REQUEST:"BODHI_API_REQUEST",API_RESPONSE:"BODHI_API_RESPONSE",STREAM_REQUEST:"BODHI_STREAM_REQUEST",STREAM_CHUNK:"BODHI_STREAM_CHUNK",STREAM_ERROR:"BODHI_STREAM_ERROR",STREAM_API_ERROR:"BODHI_STREAM_API_ERROR",ERROR:"BODHI_ERROR",EXT_REQUEST:"BODHI_EXT_REQUEST",EXT_RESPONSE:"BODHI_EXT_RESPONSE"};function v(a){return a!==null&&typeof a=="object"&&typeof a.status=="number"&&a.status>=200&&a.status<300&&"body"in a}function U(a){return a!==null&&typeof a=="object"&&a.type===m.STREAM_CHUNK}function D(a){return a!==null&&typeof a=="object"&&a.type===m.STREAM_API_ERROR}function M(a){return a!==null&&typeof a=="object"&&a.type===m.STREAM_ERROR}function y(a){return a!==null&&typeof a=="object"&&"error"in a}function q(a){return a instanceof Error&&"error"in a&&!("response"in a)&&k(a.error)}const B={GET_EXTENSION_ID:"get_extension_id",TEST_CONNECTION:"test_connection"},$="BODHI_STREAM_PORT";class K{constructor(e={},t){this.state={type:"extension",extension:"not-initialized",server:h.PENDING_EXTENSION_READY},this.extensionId=null,this.broadcastListenerActive=!1,this.config=e,this.logger=new h.Logger("ExtClient",(e==null?void 0:e.logLevel)||"warn"),this.onStateChange=t??h.NOOP_STATE_CALLBACK}setState(e){this.state=e,this.onStateChange({type:"client-state",state:e})}setAuthState(e){this.onStateChange({type:"auth-state",state:e})}setStateCallback(e){this.onStateChange=e}setupBroadcastListener(){this.broadcastListenerActive||(this.broadcastListenerActive=!0,chrome.runtime.onMessage.addListener(e=>{const t=e;return(t==null?void 0:t.type)===c.EXT2EXT_CLIENT_BROADCAST&&t.event==="authStateChanged"&&this.handleAuthStateChangedBroadcast(),!1}),this.logger.debug("Broadcast listener setup complete"))}async handleAuthStateChangedBroadcast(){this.logger.debug("Received authStateChanged broadcast, refreshing auth state");const e=await this.getAuthState();this.setAuthState(e)}generateRequestId(){return crypto.randomUUID()}getState(){return this.state}isClientInitialized(){return this.state.extension==="ready"}isServerReady(){return this.isClientInitialized()&&this.state.server.status==="ready"}async init(e={}){var i,s,o,n,E,d,u,l,T;if(!e.testConnection&&!e.selectedConnection){this.logger.info("No testConnection or selectedConnection, returning not-initialized state");const p=h.createExtensionStateNotInitialized();return this.setState(p),p}if(this.extensionId&&!e.testConnection)return this.logger.debug("Already initialized with extensionId, skipping discovery"),this.state;const t=e.timeoutMs??((s=(i=this.config.initParams)==null?void 0:i.extension)==null?void 0:s.timeoutMs)??w,r=(o=e.savedState)==null?void 0:o.extensionId;try{if(!this.extensionId){if(r)this.logger.info("Restoring with known extensionId:",r),await this.sendExtMessageWithTimeout(S.SET_EXTENSION_ID,{extensionId:r},t),this.extensionId=r;else{this.logger.info("Discovering bodhi-browser extension...");const _={attempts:(E=(n=this.config.initParams)==null?void 0:n.extension)==null?void 0:E.attempts,attemptWaitMs:(u=(d=this.config.initParams)==null?void 0:d.extension)==null?void 0:u.attemptWaitMs,attemptTimeout:(T=(l=this.config.initParams)==null?void 0:l.extension)==null?void 0:T.attemptTimeout},R=await this.sendExtMessageWithTimeout(S.DISCOVER_EXTENSION,_,t);this.extensionId=R.extensionId,this.logger.info("Extension discovered:",this.extensionId)}this.setupBroadcastListener()}const p={type:"extension",extension:"ready",extensionId:this.extensionId,server:h.PENDING_EXTENSION_READY};let g=h.PENDING_EXTENSION_READY;if(e.testConnection)try{g=await this.getServerState(),this.logger.info("Server connectivity tested, state:",g.status)}catch(_){this.logger.error("Failed to get server state:",_),g=h.BACKEND_SERVER_NOT_REACHABLE}return this.setState({...p,server:g}),this.state}catch(p){this.logger.error("Failed to initialize extension:",p),this.extensionId=null;const g=h.createExtensionStateNotFound();return this.setState(g),this.state}}async sendExtMessageWithTimeout(e,t,r=1e4){const i=new Promise((s,o)=>setTimeout(()=>o(new Error("Timeout")),r));return Promise.race([this.sendExtRequest(e,t),i])}async sendExtRequest(e,t){try{const r=this.generateRequestId(),i=await chrome.runtime.sendMessage({type:c.EXT2EXT_CLIENT_REQUEST,requestId:r,request:{action:e,params:t}});if(!i)throw h.createOperationError("No response from background script","extension_error");if(i.type!==c.EXT2EXT_CLIENT_RESPONSE)throw h.createOperationError("Invalid response type from background script","extension_error");const s=i.response;if(y(s)){const o=s.error.type||"extension_error";throw h.createOperationError(s.error.message,o)}return s}catch(r){throw q(r)?r:h.createOperationError(r instanceof Error?r.message:"Unknown error occurred","extension_error")}}async sendRawApiMessage(e,t,r,i,s){const o=this.generateRequestId();return await chrome.runtime.sendMessage({type:c.EXT2EXT_CLIENT_API_REQUEST,requestId:o,request:{method:e,endpoint:t,body:r,headers:i,authenticated:s}})}async sendApiRequest(e,t,r,i,s){const o=await this.sendRawApiMessage(e,t,r,i,s);if(P(o)){const n=o.error.type||"extension_error";return{error:{message:o.error.message,type:n}}}return o.response}async login(){return new Promise((e,t)=>{const r=async i=>{var s;if(i&&typeof i=="object"&&"type"in i&&i.type==="EXT2EXT_CLIENT_BROADCAST"&&"event"in i&&i.event==="authStateChanged"){chrome.runtime.onMessage.removeListener(r);try{const o=await this.getAuthState();if(h.isAuthError(o)){t(h.createOperationError(`Login failed: ${(s=o.error)==null?void 0:s.message}`,"auth-error"));return}if(h.isAuthLoggedOut(o)){t(h.createOperationError("Login failed: User is not logged in","auth-error"));return}this.setAuthState(o),e(o)}catch(o){t(o)}}};chrome.runtime.onMessage.addListener(r),this.sendExtRequest(S.LOGIN).catch(i=>{chrome.runtime.onMessage.removeListener(r),t(i)})})}async logout(){await this.sendExtRequest(S.LOGOUT);const e={isLoggedIn:!1};return this.setAuthState(e),e}async getAuthState(){return this.isClientInitialized()?(await this.sendExtRequest(S.GET_AUTH_STATE)).authState:h.AUTH_EXT_NOT_INITIALIZED}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(h.isApiResultOperationError(e))return{status:"not-reachable",error:e.error};if(!h.isApiResultSuccess(e))return{status:"not-reachable",error:{message:"API error from server",type:"extension_error"}};const t=e.body;switch(t.status){case"ready":return{status:"ready",version:t.version||"unknown"};case"setup":return{status:"setup",version:t.version||"unknown",error:t.error?{message:t.error.message,type:t.error.type}:{message:"Setup required",type:"extension_error"}};case"resource-admin":return{status:"resource-admin",version:t.version||"unknown",error:t.error?{message:t.error.message,type:t.error.type}:{message:"Resource admin required",type:"extension_error"}};case"error":return{status:"error",version:t.version||"unknown",error:t.error?{message:t.error.message,type:t.error.type}:{message:"Server error",type:"extension_error"}};default:return{status:"not-reachable",error:{message:"Unknown server status",type:"extension_error"}}}}async*stream(e,t,r,i,s=!0){const o=this.generateRequestId();console.log("[ExtClient] Starting stream",{method:e,endpoint:t,requestId:o});const n=chrome.runtime.connect({name:x}),d=new ReadableStream({start:u=>{n.onMessage.addListener(l=>{var T,p,g;if(l.requestId===o)switch(l.type){case c.EXT2EXT_CLIENT_STREAM_DONE:console.log("[ExtClient] Stream complete",{requestId:o}),u.close(),n.disconnect();break;case c.EXT2EXT_CLIENT_STREAM_ERROR:console.error("[ExtClient] Stream error",{requestId:o,error:JSON.stringify(l.error)}),u.error(h.createOperationError(l.error.message,"extension_error")),n.disconnect();break;case c.EXT2EXT_CLIENT_STREAM_API_ERROR:{const _=l;console.error("[ExtClient] Stream API error",{requestId:o,error:(T=_.response.body)==null?void 0:T.error}),u.error(h.createApiError(((g=(p=_.response.body)==null?void 0:p.error)==null?void 0:g.message)||"API error",_.response.status,_.response.body)),n.disconnect();break}case c.EXT2EXT_CLIENT_STREAM_CHUNK:{const _=l;v(_.response)&&u.enqueue(_.response.body);break}}}),n.onDisconnect.addListener(()=>{console.log("[ExtClient] Port disconnected",{requestId:o});try{u.error(h.createOperationError("Connection closed unexpectedly","extension_error"))}catch{}}),n.postMessage({type:c.EXT2EXT_CLIENT_STREAM_REQUEST,requestId:o,request:{method:e,endpoint:t,body:r,headers:i,authenticated:s}})}}).getReader();try{for(;;){const{done:u,value:l}=await d.read();if(u){console.log("[ExtClient] Stream iteration complete");break}yield l}}finally{d.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.extensionId??void 0}}async debug(){return{type:"ExtClient",state:this.state,authState:await this.getAuthState()}}}class V extends h.BaseFacadeClient{constructor(e,t,r,i){const s={authServerUrl:t.authServerUrl||"https://id.getbodhi.app/realms/bodhi",userScope:t.userScope||"scope_user_user",logLevel:t.logLevel||"warn",initParams:t.initParams};super(e,s,r,i)}createLogger(e){return new h.Logger("ExtUIClient",e.logLevel)}createExtClient(e,t){return new K({logLevel:e.logLevel,initParams:e.initParams},t)}createDirectClient(e,t,r){return new L({authClientId:e,authServerUrl:t.authServerUrl,userScope:t.userScope,logLevel:t.logLevel,storagePrefix:h.STORAGE_PREFIXES.EXT},r)}}const F=["bjdjhiombmfbcoeojijpfckljjghmjbf"],I=class I{constructor(e,t){this.isAuthenticating=!1,this.state="setup",this.listenersInitialized=!1,this.refreshPromise=null,this.activeStreamPorts=new Map,this.authClientId=e,this.authServerUrl=(t==null?void 0:t.authServerUrl)||"https://id.getbodhi.app/realms/bodhi",this.userScope=(t==null?void 0:t.userScope)||"scope_user_user",this.extensionId=t==null?void 0:t.extensionId,this.logger=new h.Logger("BodhiExtClient",(t==null?void 0:t.logLevel)||"warn"),this.attempts=(t==null?void 0:t.attempts)??N,this.attemptWaitMs=(t==null?void 0:t.attemptWaitMs)??O,this.attemptTimeout=(t==null?void 0:t.attemptTimeout)??X,this.authEndpoints={authorize:`${this.authServerUrl}/protocol/openid-connect/auth`,token:`${this.authServerUrl}/protocol/openid-connect/token`,userinfo:`${this.authServerUrl}/protocol/openid-connect/userinfo`,logout:`${this.authServerUrl}/protocol/openid-connect/logout`,revoke:`${this.authServerUrl}/protocol/openid-connect/revoke`},this.extensionId?this.logger.info(`[BodhiExtClient] Created client for extension: ${this.extensionId}`):this.logger.info("[BodhiExtClient] Created client without extension ID (call init() to discover)")}static base64UrlEncode(e){return btoa(String.fromCharCode(...new Uint8Array(e))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}static generateCodeVerifier(){const e=new Uint8Array(32);return crypto.getRandomValues(e),I.base64UrlEncode(e.buffer)}static async generateCodeChallenge(e){const r=new TextEncoder().encode(e),i=await crypto.subtle.digest("SHA-256",r);return I.base64UrlEncode(i)}getState(){return this.state}getExtensionIdsForEnvironment(){const t=F;return this.logger.info("[Ext2Ext/Registry] Environment: production"),this.logger.debug("[Ext2Ext/Registry] Using extension IDs:",t),t}async pingExtension(e){return this.logger.debug(`[Ext2Ext/Discovery] Pinging extension: ${e} with timeout ${this.attemptTimeout}ms`),new Promise((t,r)=>{const i=setTimeout(()=>{this.logger.debug(`[Ext2Ext/Discovery] Timeout waiting for extension ${e}`),r(new Error("Timeout"))},this.attemptTimeout);try{const s={type:m.EXT_REQUEST,requestId:crypto.randomUUID(),request:{action:"get_extension_id"}};this.logger.debug(`[Ext2Ext/Discovery] Sending message to ${e}:`,s),chrome.runtime.sendMessage(e,s,o=>{if(clearTimeout(i),chrome.runtime.lastError){this.logger.error(`[Ext2Ext/Discovery] Error from extension ${e}:`,chrome.runtime.lastError.message),r(new Error(chrome.runtime.lastError.message));return}this.logger.debug(`[Ext2Ext/Discovery] Response from ${e}:`,o);const n=o;n&&n.type===m.EXT_RESPONSE?(this.logger.debug(`[Ext2Ext/Discovery] ✓ Extension ${e} responded`),t(!0)):(this.logger.error(`[Ext2Ext/Discovery] Invalid response from ${e}:`,o),r(new Error("Invalid response")))})}catch(s){this.logger.error(`[Ext2Ext/Discovery] Exception pinging ${e}:`,s),clearTimeout(i),r(s)}})}sleep(e){return new Promise(t=>setTimeout(t,e))}async discoverBodhiExtension(e){const{attempts:t,attemptWaitMs:r,attemptTimeout:i}=e;this.logger.info(`[Ext2Ext/Discovery] Starting discovery: ${t} attempts per ID, ${i}ms timeout, ${r}ms between attempts`);const s=this.getExtensionIdsForEnvironment();this.logger.debug(`[Ext2Ext/Discovery] Will try ${s.length} extension(s):`,s);for(const E of s){for(let d=1;d<=t;d++){this.logger.debug(`[Ext2Ext/Discovery] Trying ${E} - attempt ${d}/${t}`);try{return await this.pingExtension(E),this.logger.info(`[Ext2Ext/Discovery] ✓ Found: ${E} on attempt ${d}`),{success:!0,extensionId:E}}catch(u){this.logger.debug(`[Ext2Ext/Discovery] Attempt ${d} failed for ${E}: ${u instanceof Error?u.message:"Unknown error"}`),d<t&&await this.sleep(r)}}this.logger.warn(`[Ext2Ext/Discovery] ✗ Not found: ${E} after ${t} attempts`)}const o=s.join(", "),n=`Extension not found. Tried ${s.length} IDs with ${t} attempts each: ${o}`;return this.logger.error(`[Ext2Ext/Discovery] ${n}`),{success:!1,error:n}}setupListeners(){if(this.listenersInitialized){this.logger.debug("[BodhiExtClient] Listeners already initialized, skipping");return}this.listenersInitialized=!0,chrome.runtime.onMessage.addListener((e,t,r)=>e.type!==c.EXT2EXT_CLIENT_REQUEST&&e.type!==c.EXT2EXT_CLIENT_API_REQUEST?!1:this.state!=="ready"&&!(e.type===c.EXT2EXT_CLIENT_REQUEST&&e.request.action===S.DISCOVER_EXTENSION)?(e.type===c.EXT2EXT_CLIENT_REQUEST?r({type:c.EXT2EXT_CLIENT_RESPONSE,requestId:e.requestId,response:{error:{message:this.createErrorClientNotInitialized(e),type:"NOT_INITIALIZED"}}}):r({type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:e.requestId,error:{message:`Client not initialized. Extension discovery not complete, cannot handle type:${e.type}, message:${JSON.stringify(e)}`,type:"NOT_INITIALIZED"}}),!0):(this.logger.debug(`[BodhiExtClient] Processing message.type=${e.type}`),(async()=>{const i=await this.handleAction(e);r(i)})(),!0)),chrome.runtime.onConnect.addListener(e=>{if(e.name!==x){this.logger.debug("[BodhiExtClient] Ignoring port with name:",e.name);return}this.logger.info("[BodhiExtClient] Streaming port connected"),e.onMessage.addListener(async t=>{if(t.type!==c.EXT2EXT_CLIENT_STREAM_REQUEST){this.logger.warn("[BodhiExtClient] Unknown stream message type:",t.type),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:t.requestId,error:{message:"Unknown stream message type",type:"extension_error"}});return}await this.handleStreamRequest(e,t)}),e.onDisconnect.addListener(()=>{this.logger.info("[BodhiExtClient] Streaming port disconnected")})}),this.logger.info("[BodhiExtClient] Streaming listeners initialized")}async init(e){if(this.setupListeners(),this.extensionId){this.state="ready",this.logger.warn(`[BodhiExtClient] Already initialized with extension ID: ${this.extensionId}`);return}this.logger.info("[BodhiExtClient] Starting discovery");const t={attempts:(e==null?void 0:e.attempts)??this.attempts,attemptWaitMs:(e==null?void 0:e.attemptWaitMs)??this.attemptWaitMs,attemptTimeout:(e==null?void 0:e.attemptTimeout)??this.attemptTimeout},r=await this.discoverBodhiExtension(t);if(!r.success||!r.extensionId)throw new Error(r.error||"Discovery failed");this.extensionId=r.extensionId,this.state="ready",this.logger.info(`[BodhiExtClient] ✓ Initialized: ${this.extensionId}`)}broadcastAuthStateChange(){chrome.runtime.sendMessage({type:c.EXT2EXT_CLIENT_BROADCAST,event:"authStateChanged"}).catch(e=>{this.logger.debug("[BodhiExtClient] No listeners for broadcast:",e.message)})}async getExtensionIdFromExt(){this.logger.debug("[BodhiExtClient] Getting extension ID from bodhi-browser-ext");const e=await this.sendExtRequest("get_extension_id");return this.logger.debug("[BodhiExtClient] Extension ID response:",e),e.extension_id}async handleApiRequest(e){const{requestId:t}=e;this.logger.debug("[BodhiExtClient] Handling API request:",e.request);try{let r=e.request.headers||{};if(e.request.authenticated){const s=await this._getAccessTokenRaw();if(!s)return{type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:t,error:{message:"Not authenticated. Please log in first.",type:"auth_error"}};r={...r,Authorization:`Bearer ${s}`},this.logger.debug("[BodhiExtClient] Injected auth token for authenticated request")}const i=await this.sendApiRequest(e.request.method,e.request.endpoint,e.request.body,r);return{type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:t,response:i}}catch(r){return this.logger.error("[BodhiExtClient] API request failed:",r),{type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:t,error:{message:r instanceof Error?r.message:"Unknown error",type:"network_error"}}}}async handleExtClientRequest(e){const{requestId:t,request:r}=e,{action:i,params:s}=r;this.logger.debug(`[BodhiExtClient] Handling action: ${i}`);try{let o={};switch(i){case S.DISCOVER_EXTENSION:{const n=s;await this.init(n);const E="production";this.logger.info("[BodhiExtClient] Discovery successful:",{extensionId:this.extensionId,environment:E}),o={extensionId:this.extensionId,environment:E};break}case S.SET_EXTENSION_ID:{const{extensionId:n}=s;this.extensionId=n,this.state="ready",this.logger.info("[BodhiExtClient] Extension ID set:",{extensionId:n}),o={success:!0};break}case S.GET_EXTENSION_ID:{const n=await this.sendExtRequestRaw(B.GET_EXTENSION_ID,s);return y(n.response)?{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:n.response.error.message||`Extension request failed to get extension ID: ${JSON.stringify(n.response)}`,type:n.response.error.type}}}:{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:n.response}}case S.LOGIN:await this.login(),this.broadcastAuthStateChange();break;case S.LOGOUT:await this.logout(),this.broadcastAuthStateChange();break;case S.GET_AUTH_STATE:o={authState:await this.getAuthState()};break;default:return{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:`Unknown action: ${i}`,type:"UNKNOWN_ACTION"}}}}return{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:o}}catch(o){return this.logger.error("[BodhiExtClient] Unexpected error:",o),{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:o instanceof Error?o.message:`Unexpected error: ${JSON.stringify(o)}`}}}}}async handleAction(e){switch(e.type){case c.EXT2EXT_CLIENT_API_REQUEST:return this.handleApiRequest(e);case c.EXT2EXT_CLIENT_REQUEST:return this.handleExtClientRequest(e);default:{const{requestId:t}=e;return this.logger.error("[BodhiExtClient] Unknown message type:",e.type),{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:`Unknown message type: ${e.type}`,type:"UNKNOWN_MESSAGE_TYPE"}}}}}}async login(){if(!(this.isAuthenticating||(await this.getAuthState()).isLoggedIn)){this.isAuthenticating=!0;try{if(!this.extensionId)throw new Error("Extension not discovered. Please detect Bodhi extension before login.");const t=await this.requestAccess(),r=`openid profile email roles ${this.userScope} ${t}`,i=I.generateCodeVerifier(),s=await I.generateCodeChallenge(i),o=I.generateCodeVerifier();await chrome.storage.session.set({codeVerifier:i,state:o,authInProgress:!0});const n=chrome.identity.getRedirectURL("callback"),E=new URL(this.authEndpoints.authorize);return E.searchParams.set("client_id",this.authClientId),E.searchParams.set("response_type","code"),E.searchParams.set("redirect_uri",n),E.searchParams.set("scope",r),E.searchParams.set("code_challenge",s),E.searchParams.set("code_challenge_method","S256"),E.searchParams.set("state",o),new Promise((d,u)=>{chrome.identity.launchWebAuthFlow({url:E.toString(),interactive:!0},async l=>{if(await chrome.storage.session.set({authInProgress:!1}),chrome.runtime.lastError){await chrome.storage.session.remove(["codeVerifier","state"]),u(chrome.runtime.lastError);return}if(!l){await chrome.storage.session.remove(["codeVerifier","state"]),u(new Error("No redirect URL received"));return}try{const T=new URL(l),p=T.searchParams.get("code"),g=T.searchParams.get("state"),{state:_}=await chrome.storage.session.get("state");if(g!==_){await chrome.storage.session.remove(["codeVerifier","state"]),u(new Error("State mismatch - possible CSRF"));return}if(!p){await chrome.storage.session.remove(["codeVerifier","state"]),u(new Error("No authorization code received"));return}await this.exchangeCodeForTokens(p),await chrome.storage.session.remove(["codeVerifier","state"]),d()}catch(T){await chrome.storage.session.remove(["codeVerifier","state"]),u(T)}})})}finally{this.isAuthenticating=!1}}}async exchangeCodeForTokens(e){if((await this.getAuthState()).isLoggedIn)return;const{codeVerifier:r}=await chrome.storage.session.get("codeVerifier"),i=chrome.identity.getRedirectURL("callback"),s=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:i,client_id:this.authClientId,code_verifier:r})});if(!s.ok){const n=await s.text();throw new Error(`Token exchange failed: ${s.status} ${n}`)}const o=await s.json();await this.storeTokens({accessToken:o.access_token,refreshToken:o.refresh_token,idToken:o.id_token,expiresIn:o.expires_in})}async getAuthState(){const e=await this._getAccessTokenRaw();if(!e)return{isLoggedIn:!1};try{const t=this.parseJwt(e);return{isLoggedIn:!0,userInfo:{sub:t.sub,email:t.email,name:t.name,given_name:t.given_name,family_name:t.family_name,preferred_username:t.preferred_username},accessToken:e}}catch(t){return this.logger.error("Failed to parse token:",t),{isLoggedIn:!1}}}async logout(){const{refreshToken:e}=await chrome.storage.session.get("refreshToken");if(e)try{await fetch(this.authEndpoints.revoke,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({token:e,client_id:this.authClientId,token_type_hint:"refresh_token"})})}catch(t){this.logger.warn("[OAuth] Token revocation failed:",t)}await this.clearTokens()}async sendExtRequest(e,t){const r=await this.sendExtRequestRaw(e,t);if(y(r.response))throw this.logger.error("[BodhiExtClient] Extension error:",r.response.error),new Error(r.response.error.message||`Extension request failed: ${JSON.stringify(r.response)}`);return r.response}async sendApiRequest(e,t,r,i){if(!this.extensionId)throw new Error(this.createErrorClientNotInitialized({type:"api",method:e,endpoint:t}));this.logger.debug(`[BodhiExtClient] Sending API_REQUEST: method=${e}, endpoint=${t}`,r?{body:r}:"");const s=crypto.randomUUID(),o={type:m.API_REQUEST,requestId:s,request:{method:e,endpoint:t,body:r,headers:i}};return this.logger.debug(`[BodhiExtClient] Request ID: ${s}, Extension: ${this.extensionId}`),new Promise((n,E)=>{try{chrome.runtime.sendMessage(this.extensionId,o,d=>{if(chrome.runtime.lastError){this.logger.error(`[BodhiExtClient] Chrome runtime error for request ${s}:`,chrome.runtime.lastError),E(new Error(chrome.runtime.lastError.message));return}if(this.logger.debug(`[BodhiExtClient] Response for request ${s}:`,d),!d){this.logger.error(`[BodhiExtClient] No response received for request ${s}`),E(new Error("No response from extension"));return}d.type===m.API_RESPONSE&&d.requestId===s?"error"in d?(this.logger.error(`[BodhiExtClient] API error for ${s}:`,d.error),E(new Error(d.error.message))):(this.logger.debug(`[BodhiExtClient] ✓ Valid API_RESPONSE for ${s}`),n(d.response)):(this.logger.error(`[BodhiExtClient] Invalid response format for ${s}:`,d),E(new Error("Invalid response format")))})}catch(d){this.logger.error(`[BodhiExtClient] Exception sending message for ${s}:`,d),E(d)}})}async sendExtRequestRaw(e,t){if(!this.extensionId)throw new Error(this.createErrorClientNotInitialized({type:"ext",action:e,params:t}));this.logger.debug(`[BodhiExtClient] Sending EXT_REQUEST (raw): action=${e}`,t?{params:t}:"");const r=crypto.randomUUID(),i={type:m.EXT_REQUEST,requestId:r,request:{action:e,params:t}};return this.logger.debug(`[BodhiExtClient] Request ID: ${r}, Extension: ${this.extensionId}`),new Promise((s,o)=>{try{chrome.runtime.sendMessage(this.extensionId,i,n=>{if(chrome.runtime.lastError){this.logger.error(`[BodhiExtClient] Chrome runtime error for request ${r}:`,chrome.runtime.lastError),o(new Error(chrome.runtime.lastError.message));return}if(this.logger.debug(`[BodhiExtClient] Response for request ${r}:`,n),!n){this.logger.error(`[BodhiExtClient] No response received for request ${r}`),o(new Error("No response from extension"));return}n.type===m.EXT_RESPONSE&&n.requestId===r?(this.logger.debug(`[BodhiExtClient] ✓ Valid EXT_RESPONSE for ${r}`),s(n)):(this.logger.error(`[BodhiExtClient] Invalid response format for ${r}:`,n),o(new Error("Invalid response format")))})}catch(n){this.logger.error(`[BodhiExtClient] Exception sending message for ${r}:`,n),o(n)}})}async handleStreamRequest(e,t){const{requestId:r,request:i}=t,{method:s,endpoint:o,body:n,headers:E,authenticated:d}=i;this.logger.debug("[BodhiExtClient] Processing stream request:",{requestId:r,method:s,endpoint:o,authenticated:d}),this.extensionId||e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:this.createErrorClientNotInitialized(t),type:"extension_error"}});try{let u={...E};if(d!==!1){const g=await this._getAccessTokenRaw();if(!g){e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:"Not authenticated. Please log in first.",type:"extension_error"}});return}u={...u,Authorization:`Bearer ${g}`},this.logger.debug("[BodhiExtClient] Injected auth token for authenticated request")}const l=chrome.runtime.connect(this.extensionId,{name:$});this.activeStreamPorts.set(r,l);const T=setTimeout(()=>{this.activeStreamPorts.has(r)&&(this.logger.error(`[BodhiExtClient] Stream timeout for ${r}`),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:"Stream request timed out",type:"timeout_error"}}),this.cleanupStreamPort(r))},I.STREAM_TIMEOUT);l.onMessage.addListener(g=>{if(U(g)){const _=g.response,R=_.body;_.status>=400?e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_API_ERROR,requestId:r,response:_}):R!=null&&R.done?(e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_DONE,requestId:r}),this.logger.info(`[BodhiExtClient] Stream complete for ${r}`),clearTimeout(T),this.cleanupStreamPort(r)):e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_CHUNK,requestId:r,response:_})}else D(g)?(this.logger.error(`[BodhiExtClient] Stream API error for ${r}: ${g.response.status}`),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_API_ERROR,requestId:r,response:g.response})):M(g)&&(this.logger.error(`[BodhiExtClient] Stream error for ${r}:`,g.error.message),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:`stream error: ${JSON.stringify(g)}`,type:"extension_error"}}),clearTimeout(T),this.cleanupStreamPort(r))}),l.onDisconnect.addListener(()=>{clearTimeout(T),this.activeStreamPorts.has(r)&&(this.logger.error(`[BodhiExtClient] Bodhi port disconnected for ${r}`),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:"Connection to Bodhi extension closed unexpectedly",type:"network_error"}}),this.activeStreamPorts.delete(r))});const p={type:m.STREAM_REQUEST,requestId:r,request:{method:s,endpoint:o,body:n,headers:u}};this.logger.debug("[BodhiExtClient] Sending stream request to bodhi port:",p),l.postMessage(p)}catch(u){const l=u;this.logger.error("[BodhiExtClient] Stream error:",JSON.stringify(l.message)),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:`uncaught error: ${JSON.stringify({error:l,message:l.message})}`,type:"extension_error"}})}}cleanupStreamPort(e){const t=this.activeStreamPorts.get(e);if(t){try{t.disconnect()}catch{}this.activeStreamPorts.delete(e)}}async requestAccess(){const e=await this.sendApiRequest("POST","/bodhi/v1/apps/request-access",{app_client_id:this.authClientId});if(!v(e))throw this.logger.error("[BodhiExtClient] Failed to get resource access scope: API error"),new Error("Failed to get resource access scope: API error");return e.body.scope}async storeTokens(e){const t=Date.now()+(e.expiresIn||3600)*1e3;await chrome.storage.session.set({accessToken:e.accessToken,refreshToken:e.refreshToken,idToken:e.idToken,expiresAt:t})}async _getAccessTokenRaw(){const{accessToken:e,expiresAt:t}=await chrome.storage.session.get(["accessToken","expiresAt"]);if(!e||!t)return null;if(Date.now()>=t-5*1e3){const{refreshToken:r}=await chrome.storage.session.get("refreshToken");return r?this._tryRefreshToken(r):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 h.refreshAccessToken(this.authEndpoints.token,e,this.authClientId);if(t)return await this._storeRefreshedTokens(t),this.logger.info("Token refreshed successfully"),this.broadcastAuthStateChange(),t.access_token}catch(t){this.logger.warn("Token refresh failed:",t)}throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"),h.createOperationError("Access token expired and unable to refresh. Try logging out and logging in again.","token_refresh_failed")}async _storeRefreshedTokens(e){const t=Date.now()+e.expires_in*1e3,r={accessToken:e.access_token,expiresAt:t};e.refresh_token&&(r.refreshToken=e.refresh_token),e.id_token&&(r.idToken=e.id_token),await chrome.storage.session.set(r)}async clearTokens(){await chrome.storage.session.remove(["accessToken","refreshToken","idToken","expiresAt","codeVerifier","state","authInProgress","bodhiUserInfo"])}parseJwt(e){const r=e.split(".")[1].replace(/-/g,"+").replace(/_/g,"/"),i=decodeURIComponent(atob(r).split("").map(s=>"%"+("00"+s.charCodeAt(0).toString(16)).slice(-2)).join(""));return JSON.parse(i)}createErrorClientNotInitialized(e){return`Client not initialized. Extension discovery not triggered nor extensionId set, cannot handle request: ${JSON.stringify(e)}`}};I.STREAM_TIMEOUT=12e4;let f=I;exports.BodhiExtClient=f;exports.DISCOVERY_ATTEMPTS=N;exports.DISCOVERY_ATTEMPT_TIMEOUT=X;exports.DISCOVERY_ATTEMPT_WAIT_MS=O;exports.DISCOVERY_TIMEOUT_MS=w;exports.EXT2EXT_CLIENT_ACTIONS=S;exports.EXT2EXT_CLIENT_MESSAGE_TYPES=c;exports.EXT2EXT_CLIENT_STREAM_PORT=x;exports.ExtUIClient=V;exports.isExtClientApiError=P;