@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.
- package/dist/bodhi-ext.cjs.js +1 -1911
- package/dist/bodhi-ext.esm.js +769 -1093
- package/package.json +2 -2
- package/dist/bodhi-ext.cjs.js.map +0 -1
- package/dist/bodhi-ext.esm.js.map +0 -1
package/dist/bodhi-ext.cjs.js
CHANGED
|
@@ -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;
|