@hanzo/browser-extension 1.7.2 → 1.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/browser-extension/background-firefox.js +426 -168
  2. package/dist/browser-extension/background.js +38 -43
  3. package/dist/browser-extension/chrome/background.js +38 -43
  4. package/dist/browser-extension/chrome/manifest.json +1 -1
  5. package/dist/browser-extension/chrome/popup.js +18 -7
  6. package/dist/browser-extension/chrome/sidebar.js +12 -11
  7. package/dist/browser-extension/firefox/background.js +426 -168
  8. package/dist/browser-extension/firefox/manifest.json +1 -1
  9. package/dist/browser-extension/firefox/popup.js +18 -7
  10. package/dist/browser-extension/firefox/sidebar.js +12 -11
  11. package/dist/browser-extension/manifest.json +1 -1
  12. package/dist/browser-extension/package.json +1 -1
  13. package/dist/browser-extension/popup.js +18 -7
  14. package/dist/browser-extension/safari/background.js +38 -43
  15. package/dist/browser-extension/safari/popup.js +18 -7
  16. package/dist/browser-extension/safari/sidebar.js +12 -11
  17. package/dist/browser-extension/sidebar.js +12 -11
  18. package/dist/hanzo-ai-chrome-1.7.2.zip +0 -0
  19. package/dist/hanzo-ai-chrome-1.7.3.zip +0 -0
  20. package/dist/hanzo-ai-firefox-1.7.2.zip +0 -0
  21. package/dist/hanzo-ai-firefox-1.7.3.zip +0 -0
  22. package/dist/safari/Hanzo AI/Hanzo AI.xcodeproj/project.pbxproj +295 -295
  23. package/dist/safari/Hanzo AI/Shared (App)/ViewController.swift +1 -1
  24. package/dist/safari/Hanzo AI/Shared (Extension)/SafariWebExtensionHandler.swift +1 -1
  25. package/dist/safari/Hanzo AI/iOS (App)/AppDelegate.swift +1 -1
  26. package/dist/safari/Hanzo AI/iOS (App)/SceneDelegate.swift +1 -1
  27. package/dist/safari/Hanzo AI/macOS (App)/AppDelegate.swift +1 -1
  28. package/hanzo-ai-chrome-v1.7.6.zip +0 -0
  29. package/hanzo-ai-firefox-v1.7.6.zip +0 -0
  30. package/package.json +1 -1
  31. package/src/manifest-firefox.json +1 -1
  32. package/src/manifest.json +1 -1
  33. package/src/package.json +1 -1
  34. package/src/chat-widget.tsx +0 -93
  35. package/src/popup.js +0 -192
  36. package/src/sidebar.js +0 -884
@@ -1,5 +1,253 @@
1
1
  "use strict";
2
2
  (() => {
3
+ // src/auth.ts
4
+ function generateRandomString(length) {
5
+ const array = new Uint8Array(length);
6
+ crypto.getRandomValues(array);
7
+ return Array.from(array, (b) => b.toString(36).padStart(2, "0")).join("").slice(0, length);
8
+ }
9
+ async function sha256(plain) {
10
+ const encoder = new TextEncoder();
11
+ return crypto.subtle.digest("SHA-256", encoder.encode(plain));
12
+ }
13
+ function base64UrlEncode(buffer) {
14
+ const bytes = new Uint8Array(buffer);
15
+ let binary = "";
16
+ for (const byte of bytes) {
17
+ binary += String.fromCharCode(byte);
18
+ }
19
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
20
+ }
21
+ async function generatePkceChallenge() {
22
+ const codeVerifier = generateRandomString(64);
23
+ const hash = await sha256(codeVerifier);
24
+ const codeChallenge = base64UrlEncode(hash);
25
+ return { codeVerifier, codeChallenge };
26
+ }
27
+ function generateState() {
28
+ return generateRandomString(32);
29
+ }
30
+ var IAM_LOGIN = "https://hanzo.id";
31
+ var IAM_API = "https://iam.hanzo.ai";
32
+ var CLIENT_ID = "app-hanzo";
33
+ var SCOPES = "openid profile email";
34
+ function getRedirectUri() {
35
+ return "https://hanzo.ai/callback";
36
+ }
37
+ var STORAGE_KEYS = {
38
+ accessToken: "hanzo_iam_access_token",
39
+ refreshToken: "hanzo_iam_refresh_token",
40
+ idToken: "hanzo_iam_id_token",
41
+ expiresAt: "hanzo_iam_expires_at",
42
+ user: "hanzo_iam_user"
43
+ };
44
+ async function storeTokens(tokens) {
45
+ const data = {
46
+ [STORAGE_KEYS.accessToken]: tokens.access_token
47
+ };
48
+ if (tokens.refresh_token) data[STORAGE_KEYS.refreshToken] = tokens.refresh_token;
49
+ if (tokens.id_token) data[STORAGE_KEYS.idToken] = tokens.id_token;
50
+ if (tokens.expires_in) {
51
+ data[STORAGE_KEYS.expiresAt] = Date.now() + tokens.expires_in * 1e3;
52
+ }
53
+ return chrome.storage.local.set(data);
54
+ }
55
+ async function getStoredTokens() {
56
+ return new Promise((resolve) => {
57
+ chrome.storage.local.get(
58
+ [STORAGE_KEYS.accessToken, STORAGE_KEYS.refreshToken, STORAGE_KEYS.expiresAt],
59
+ (result) => {
60
+ resolve({
61
+ accessToken: result[STORAGE_KEYS.accessToken] || null,
62
+ refreshToken: result[STORAGE_KEYS.refreshToken] || null,
63
+ expiresAt: result[STORAGE_KEYS.expiresAt] || null
64
+ });
65
+ }
66
+ );
67
+ });
68
+ }
69
+ async function login() {
70
+ const { codeVerifier, codeChallenge } = await generatePkceChallenge();
71
+ const state = generateState();
72
+ const redirectUri = getRedirectUri();
73
+ const authorizeUrl = new URL(`${IAM_LOGIN}/login/oauth/authorize`);
74
+ authorizeUrl.searchParams.set("client_id", CLIENT_ID);
75
+ authorizeUrl.searchParams.set("response_type", "code");
76
+ authorizeUrl.searchParams.set("redirect_uri", redirectUri);
77
+ authorizeUrl.searchParams.set("code_challenge", codeChallenge);
78
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
79
+ authorizeUrl.searchParams.set("scope", SCOPES);
80
+ authorizeUrl.searchParams.set("state", state);
81
+ const callbackUrl = await openAuthTab(authorizeUrl.toString(), redirectUri);
82
+ const url = new URL(callbackUrl);
83
+ const returnedState = url.searchParams.get("state");
84
+ if (returnedState !== state) throw new Error("State mismatch \u2014 possible CSRF");
85
+ const code = url.searchParams.get("code");
86
+ const directToken = url.searchParams.get("access_token");
87
+ if (directToken) {
88
+ const tokens = {
89
+ access_token: directToken,
90
+ refresh_token: url.searchParams.get("refresh_token") || void 0,
91
+ token_type: "Bearer"
92
+ };
93
+ await storeTokens(tokens);
94
+ } else if (code) {
95
+ const tokenResponse = await fetch(`${IAM_API}/api/login/oauth/access_token`, {
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify({
99
+ grant_type: "authorization_code",
100
+ client_id: CLIENT_ID,
101
+ code,
102
+ redirect_uri: redirectUri,
103
+ code_verifier: codeVerifier
104
+ })
105
+ });
106
+ if (!tokenResponse.ok) {
107
+ const errText = await tokenResponse.text();
108
+ throw new Error(`Token exchange failed: ${errText}`);
109
+ }
110
+ const tokens = await tokenResponse.json();
111
+ await storeTokens(tokens);
112
+ } else {
113
+ const error = url.searchParams.get("error_description") || url.searchParams.get("error") || "No authorization code or token";
114
+ throw new Error(error);
115
+ }
116
+ const { accessToken } = await getStoredTokens();
117
+ if (!accessToken) throw new Error("Login succeeded but no token was stored");
118
+ const user = await fetchUserInfo(accessToken);
119
+ await chrome.storage.local.set({ [STORAGE_KEYS.user]: user });
120
+ return user;
121
+ }
122
+ function openAuthTab(authorizeUrl, redirectUriPrefix) {
123
+ return new Promise((resolve, reject) => {
124
+ let authTabId;
125
+ const timeout = setTimeout(() => {
126
+ cleanup();
127
+ reject(new Error("Login timed out"));
128
+ }, 3e5);
129
+ function cleanup() {
130
+ clearTimeout(timeout);
131
+ chrome.tabs.onUpdated.removeListener(onTabUpdated);
132
+ chrome.tabs.onRemoved.removeListener(onTabRemoved);
133
+ }
134
+ function onTabUpdated(tabId, changeInfo) {
135
+ if (tabId !== authTabId || !changeInfo.url) return;
136
+ if (changeInfo.url.startsWith(redirectUriPrefix)) {
137
+ const callbackUrl = changeInfo.url;
138
+ cleanup();
139
+ chrome.tabs.remove(tabId).catch(() => {
140
+ });
141
+ resolve(callbackUrl);
142
+ }
143
+ }
144
+ function onTabRemoved(tabId) {
145
+ if (tabId !== authTabId) return;
146
+ cleanup();
147
+ reject(new Error("Login cancelled"));
148
+ }
149
+ chrome.tabs.onUpdated.addListener(onTabUpdated);
150
+ chrome.tabs.onRemoved.addListener(onTabRemoved);
151
+ chrome.tabs.create({ url: authorizeUrl }, (tab) => {
152
+ if (chrome.runtime.lastError || !tab?.id) {
153
+ cleanup();
154
+ reject(new Error(chrome.runtime.lastError?.message || "Failed to open login tab"));
155
+ return;
156
+ }
157
+ authTabId = tab.id;
158
+ });
159
+ });
160
+ }
161
+ async function logout() {
162
+ await chrome.storage.local.remove(Object.values(STORAGE_KEYS));
163
+ }
164
+ async function getValidAccessToken() {
165
+ const { accessToken, refreshToken, expiresAt } = await getStoredTokens();
166
+ if (!accessToken) return null;
167
+ if (expiresAt && Date.now() < expiresAt - 6e4) {
168
+ return accessToken;
169
+ }
170
+ if (refreshToken) {
171
+ try {
172
+ const refreshed = await refreshAccessToken(refreshToken);
173
+ return refreshed;
174
+ } catch {
175
+ await logout();
176
+ return null;
177
+ }
178
+ }
179
+ return accessToken;
180
+ }
181
+ async function refreshAccessToken(refreshToken) {
182
+ const response = await fetch(`${IAM_API}/api/login/oauth/access_token`, {
183
+ method: "POST",
184
+ headers: { "Content-Type": "application/json" },
185
+ body: JSON.stringify({
186
+ grant_type: "refresh_token",
187
+ client_id: CLIENT_ID,
188
+ refresh_token: refreshToken
189
+ })
190
+ });
191
+ if (!response.ok) throw new Error("Token refresh failed");
192
+ const tokens = await response.json();
193
+ await storeTokens(tokens);
194
+ return tokens.access_token;
195
+ }
196
+ async function fetchUserInfo(accessToken) {
197
+ const response = await fetch(`${IAM_API}/api/userinfo`, {
198
+ headers: { Authorization: `Bearer ${accessToken}` }
199
+ });
200
+ if (!response.ok) throw new Error("Failed to fetch user info");
201
+ return response.json();
202
+ }
203
+ async function getUserInfo() {
204
+ return new Promise((resolve) => {
205
+ chrome.storage.local.get([STORAGE_KEYS.user], (result) => {
206
+ resolve(result[STORAGE_KEYS.user] || null);
207
+ });
208
+ });
209
+ }
210
+ async function isAuthenticated() {
211
+ const { accessToken } = await getStoredTokens();
212
+ return !!accessToken;
213
+ }
214
+ async function getAuthStatus() {
215
+ const [authenticated, user] = await Promise.all([
216
+ isAuthenticated(),
217
+ getUserInfo()
218
+ ]);
219
+ return { authenticated, user };
220
+ }
221
+
222
+ // src/chat-client.ts
223
+ var API_BASE = "https://api.hanzo.ai";
224
+ async function listModels(token) {
225
+ const response = await fetch(`${API_BASE}/v1/models`, {
226
+ headers: { Authorization: `Bearer ${token}` }
227
+ });
228
+ if (!response.ok) {
229
+ throw new Error(`Failed to list models: ${response.status}`);
230
+ }
231
+ const data = await response.json();
232
+ return data.data || data.models || data || [];
233
+ }
234
+ async function chatCompletion(token, options) {
235
+ const response = await fetch(`${API_BASE}/v1/chat/completions`, {
236
+ method: "POST",
237
+ headers: {
238
+ "Content-Type": "application/json",
239
+ Authorization: `Bearer ${token}`
240
+ },
241
+ body: JSON.stringify(options)
242
+ });
243
+ if (!response.ok) {
244
+ const text = await response.text();
245
+ throw new Error(`Chat API error ${response.status}: ${text}`);
246
+ }
247
+ const data = await response.json();
248
+ return data.choices?.[0]?.message?.content || "";
249
+ }
250
+
3
251
  // src/background-firefox.ts
4
252
  var controlSession = {
5
253
  active: false,
@@ -13,7 +261,14 @@
13
261
  this.cdpPort = 9223;
14
262
  this.reconnectTimeout = 3e3;
15
263
  console.log("[Hanzo] Firefox extension initializing...");
16
- this.connect();
264
+ this.loadPort().then(() => this.connect());
265
+ }
266
+ async loadPort() {
267
+ try {
268
+ const result = await browser.storage.local.get(["cdpPort"]);
269
+ if (result.cdpPort) this.cdpPort = result.cdpPort;
270
+ } catch {
271
+ }
17
272
  }
18
273
  connect() {
19
274
  const url = `ws://localhost:${this.cdpPort}/cdp`;
@@ -34,16 +289,23 @@
34
289
  this.send({ id: 0, error: { code: -1, message: String(e) } });
35
290
  }
36
291
  };
37
- this.wsConnection.onerror = (error) => {
38
- console.error("[Hanzo] WebSocket error:", error);
292
+ this.wsConnection.onerror = () => {
39
293
  };
40
294
  this.wsConnection.onclose = () => {
41
295
  console.log("[Hanzo] Disconnected, reconnecting...");
42
296
  setTimeout(() => this.connect(), this.reconnectTimeout);
43
297
  };
44
298
  }
299
+ isConnected() {
300
+ return this.wsConnection?.readyState === WebSocket.OPEN;
301
+ }
302
+ sendToWs(data) {
303
+ if (this.wsConnection?.readyState === WebSocket.OPEN) {
304
+ this.wsConnection.send(JSON.stringify(data));
305
+ }
306
+ }
45
307
  register() {
46
- this.send({
308
+ this.sendToWs({
47
309
  type: "register",
48
310
  role: "cdp-provider",
49
311
  capabilities: [
@@ -61,9 +323,7 @@
61
323
  });
62
324
  }
63
325
  send(data) {
64
- if (this.wsConnection?.readyState === WebSocket.OPEN) {
65
- this.wsConnection.send(JSON.stringify(data));
66
- }
326
+ this.sendToWs(data);
67
327
  }
68
328
  async handleCommand(message) {
69
329
  const { id, method, params = {} } = message;
@@ -74,9 +334,6 @@
74
334
  return { id, error: { code: -1, message: String(e) } };
75
335
  }
76
336
  }
77
- /**
78
- * Wrap executeScript with a timeout to prevent blocking on GPU-heavy pages
79
- */
80
337
  executeScriptWithTimeout(tabId, code, timeoutMs = 1e4) {
81
338
  return Promise.race([
82
339
  browser.tabs.executeScript(tabId, { code }),
@@ -102,17 +359,12 @@
102
359
  }))
103
360
  };
104
361
  }
105
- // Native tab property reads - never block on page content
106
362
  case "hanzo.url": {
107
- if (activeTab) {
108
- return { result: { value: activeTab.url || "" } };
109
- }
363
+ if (activeTab) return { result: { value: activeTab.url || "" } };
110
364
  throw new Error("No active tab");
111
365
  }
112
366
  case "hanzo.title": {
113
- if (activeTab) {
114
- return { result: { value: activeTab.title || "" } };
115
- }
367
+ if (activeTab) return { result: { value: activeTab.title || "" } };
116
368
  throw new Error("No active tab");
117
369
  }
118
370
  case "hanzo.tabInfo": {
@@ -164,11 +416,7 @@
164
416
  if (activeTab?.id) {
165
417
  const expression = params.expression;
166
418
  const timeout = params.timeout || 1e4;
167
- const results = await this.executeScriptWithTimeout(
168
- activeTab.id,
169
- expression,
170
- timeout
171
- );
419
+ const results = await this.executeScriptWithTimeout(activeTab.id, expression, timeout);
172
420
  return { result: { value: results[0] } };
173
421
  }
174
422
  throw new Error("No active tab");
@@ -202,17 +450,14 @@
202
450
  const value = this.escapeValue(params.value || "");
203
451
  this.notifyOverlay(activeTab.id, "ai.control.highlight", { selector: params.selector });
204
452
  this.notifyOverlay(activeTab.id, "ai.control.status", { text: `Filling ${params.selector}` });
205
- await this.executeScriptWithTimeout(
206
- activeTab.id,
207
- `
208
- var el = document.querySelector('${selector}');
209
- if (el) {
210
- el.value = '${value}';
211
- el.dispatchEvent(new Event('input', {bubbles: true}));
212
- el.dispatchEvent(new Event('change', {bubbles: true}));
213
- }
214
- `
215
- );
453
+ await this.executeScriptWithTimeout(activeTab.id, `
454
+ var el = document.querySelector('${selector}');
455
+ if (el) {
456
+ el.value = '${value}';
457
+ el.dispatchEvent(new Event('input', {bubbles: true}));
458
+ el.dispatchEvent(new Event('change', {bubbles: true}));
459
+ }
460
+ `);
216
461
  return { success: true };
217
462
  }
218
463
  throw new Error("No active tab or selector");
@@ -232,16 +477,13 @@
232
477
  case "hanzo.type": {
233
478
  if (activeTab?.id && params.text) {
234
479
  const text = this.escapeValue(params.text);
235
- await this.executeScriptWithTimeout(
236
- activeTab.id,
237
- `
238
- var el = document.activeElement;
239
- if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) {
240
- el.value += '${text}';
241
- el.dispatchEvent(new Event('input', {bubbles: true}));
242
- }
243
- `
244
- );
480
+ await this.executeScriptWithTimeout(activeTab.id, `
481
+ var el = document.activeElement;
482
+ if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) {
483
+ el.value += '${text}';
484
+ el.dispatchEvent(new Event('input', {bubbles: true}));
485
+ }
486
+ `);
245
487
  return { success: true };
246
488
  }
247
489
  throw new Error("No active tab or text");
@@ -309,31 +551,6 @@
309
551
  return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
310
552
  }
311
553
  };
312
- var IAM_BASE = "https://hanzo.id";
313
- var API_BASE = "https://api.hanzo.ai";
314
- var CLIENT_ID = "app-hanzo";
315
- var SCOPES = "openid profile email";
316
- var STORAGE_KEYS = {
317
- accessToken: "hanzo_iam_access_token",
318
- refreshToken: "hanzo_iam_refresh_token",
319
- idToken: "hanzo_iam_id_token",
320
- expiresAt: "hanzo_iam_expires_at",
321
- user: "hanzo_iam_user"
322
- };
323
- function generateRandomString(length) {
324
- const array = new Uint8Array(length);
325
- crypto.getRandomValues(array);
326
- return Array.from(array, (b) => b.toString(36).padStart(2, "0")).join("").slice(0, length);
327
- }
328
- async function sha256(plain) {
329
- return crypto.subtle.digest("SHA-256", new TextEncoder().encode(plain));
330
- }
331
- function base64UrlEncode(buffer) {
332
- const bytes = new Uint8Array(buffer);
333
- let binary = "";
334
- for (const byte of bytes) binary += String.fromCharCode(byte);
335
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
336
- }
337
554
  async function getRagConfig() {
338
555
  const result = await browser.storage.local.get([
339
556
  "hanzo_rag_endpoint",
@@ -376,9 +593,7 @@
376
593
  }
377
594
  async function queryRagEndpoint(params) {
378
595
  if (!params.endpoint) return [];
379
- const headers = {
380
- "Content-Type": "application/json"
381
- };
596
+ const headers = { "Content-Type": "application/json" };
382
597
  if (params.apiKey) headers.Authorization = `Bearer ${params.apiKey}`;
383
598
  const response = await fetch(params.endpoint, {
384
599
  method: "POST",
@@ -401,117 +616,129 @@
401
616
  source: snippet.source || "rag-endpoint"
402
617
  }));
403
618
  }
404
- async function firefoxLogin() {
405
- const codeVerifier = generateRandomString(64);
406
- const codeChallenge = base64UrlEncode(await sha256(codeVerifier));
407
- const state = generateRandomString(32);
408
- const redirectUri = "https://hanzo.ai/callback";
409
- const authorizeUrl = new URL(`${IAM_BASE}/login/oauth/authorize`);
410
- authorizeUrl.searchParams.set("client_id", CLIENT_ID);
411
- authorizeUrl.searchParams.set("response_type", "code");
412
- authorizeUrl.searchParams.set("redirect_uri", redirectUri);
413
- authorizeUrl.searchParams.set("code_challenge", codeChallenge);
414
- authorizeUrl.searchParams.set("code_challenge_method", "S256");
415
- authorizeUrl.searchParams.set("scope", SCOPES);
416
- authorizeUrl.searchParams.set("state", state);
417
- const callbackUrl = await new Promise((resolve, reject) => {
418
- let authTabId;
419
- const timeout = setTimeout(() => {
420
- cleanup();
421
- reject(new Error("Login timed out"));
422
- }, 3e5);
423
- function cleanup() {
424
- clearTimeout(timeout);
425
- browser.tabs.onUpdated.removeListener(onUpdated);
426
- browser.tabs.onRemoved.removeListener(onRemoved);
427
- }
428
- function onUpdated(tabId, changeInfo) {
429
- if (tabId !== authTabId || !changeInfo.url) return;
430
- if (changeInfo.url.startsWith(redirectUri)) {
431
- cleanup();
432
- browser.tabs.remove(tabId).catch(() => {
433
- });
434
- resolve(changeInfo.url);
435
- }
436
- }
437
- function onRemoved(tabId) {
438
- if (tabId !== authTabId) return;
439
- cleanup();
440
- reject(new Error("Login cancelled"));
441
- }
442
- browser.tabs.onUpdated.addListener(onUpdated);
443
- browser.tabs.onRemoved.addListener(onRemoved);
444
- browser.tabs.create({ url: authorizeUrl.toString() }).then((tab) => {
445
- authTabId = tab.id;
446
- });
447
- });
448
- const url = new URL(callbackUrl);
449
- if (url.searchParams.get("state") !== state) throw new Error("State mismatch");
450
- const code = url.searchParams.get("code");
451
- if (!code) throw new Error(url.searchParams.get("error_description") || "No code");
452
- const tokenResponse = await fetch(`${IAM_BASE}/oauth/token`, {
453
- method: "POST",
454
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
455
- body: new URLSearchParams({
456
- grant_type: "authorization_code",
457
- client_id: CLIENT_ID,
458
- code,
459
- redirect_uri: redirectUri,
460
- code_verifier: codeVerifier
461
- })
462
- });
463
- if (!tokenResponse.ok) throw new Error(`Token exchange failed: ${await tokenResponse.text()}`);
464
- const tokens = await tokenResponse.json();
465
- const data = { [STORAGE_KEYS.accessToken]: tokens.access_token };
466
- if (tokens.refresh_token) data[STORAGE_KEYS.refreshToken] = tokens.refresh_token;
467
- if (tokens.id_token) data[STORAGE_KEYS.idToken] = tokens.id_token;
468
- if (tokens.expires_in) data[STORAGE_KEYS.expiresAt] = Date.now() + tokens.expires_in * 1e3;
469
- await browser.storage.local.set(data);
470
- const userResp = await fetch(`${IAM_BASE}/oauth/userinfo`, { headers: { Authorization: `Bearer ${tokens.access_token}` } });
471
- const user = userResp.ok ? await userResp.json() : {};
472
- await browser.storage.local.set({ [STORAGE_KEYS.user]: user });
473
- return user;
474
- }
475
619
  browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
476
620
  (async () => {
477
621
  switch (request.action) {
622
+ // --- Auth (shared module — same code as Chrome) ---
478
623
  case "auth.login":
479
624
  try {
480
- const user = await firefoxLogin();
625
+ const user = await login();
481
626
  sendResponse({ success: true, user });
482
627
  } catch (e) {
483
628
  sendResponse({ success: false, error: e.message });
484
629
  }
485
630
  break;
486
631
  case "auth.logout":
487
- await browser.storage.local.remove(Object.values(STORAGE_KEYS));
632
+ try {
633
+ await logout();
634
+ sendResponse({ success: true });
635
+ } catch (e) {
636
+ sendResponse({ success: false, error: e.message });
637
+ }
638
+ break;
639
+ case "auth.status":
640
+ try {
641
+ const status = await getAuthStatus();
642
+ sendResponse({ success: true, ...status });
643
+ } catch (e) {
644
+ sendResponse({ success: false, error: e.message });
645
+ }
646
+ break;
647
+ case "auth.getToken":
648
+ try {
649
+ const token = await getValidAccessToken();
650
+ sendResponse({ success: !!token, token });
651
+ } catch (e) {
652
+ sendResponse({ success: false, error: e.message });
653
+ }
654
+ break;
655
+ // --- Cloud Chat (shared module) ---
656
+ case "chat.listModels":
657
+ try {
658
+ const token = await getValidAccessToken();
659
+ if (!token) {
660
+ sendResponse({ success: false, error: "Not authenticated" });
661
+ break;
662
+ }
663
+ const models = await listModels(token);
664
+ sendResponse({ success: true, models });
665
+ } catch (e) {
666
+ sendResponse({ success: false, error: e.message });
667
+ }
668
+ break;
669
+ case "chat.complete":
670
+ try {
671
+ const token = await getValidAccessToken();
672
+ if (!token) {
673
+ sendResponse({ success: false, error: "Not authenticated" });
674
+ break;
675
+ }
676
+ const model = String(request.model || "gpt-4o");
677
+ const messagesInput = Array.isArray(request.messages) ? request.messages : [];
678
+ const messages = messagesInput.filter((m) => m && typeof m.content === "string" && typeof m.role === "string").map((m) => ({
679
+ role: m.role === "system" || m.role === "assistant" ? m.role : "user",
680
+ content: String(m.content)
681
+ })).slice(-24);
682
+ if (!messages.length) {
683
+ sendResponse({ success: false, error: "messages are required" });
684
+ break;
685
+ }
686
+ const content = await chatCompletion(token, {
687
+ model,
688
+ messages,
689
+ temperature: typeof request.temperature === "number" ? request.temperature : void 0,
690
+ max_tokens: typeof request.max_tokens === "number" ? request.max_tokens : void 0
691
+ });
692
+ sendResponse({ success: true, content });
693
+ } catch (e) {
694
+ sendResponse({ success: false, error: e.message });
695
+ }
696
+ break;
697
+ // --- ZAP Protocol (not available on Firefox — return proper status) ---
698
+ case "zap.status":
699
+ sendResponse({ success: true, zap: { connected: false, mcps: [], extensionId: "firefox" } });
700
+ break;
701
+ case "zap.discover":
702
+ sendResponse({ success: true, mcps: [] });
703
+ break;
704
+ case "zap.listTools":
705
+ sendResponse({ success: true, tools: [] });
706
+ break;
707
+ case "zap.connect":
708
+ case "zap.callTool":
709
+ sendResponse({ success: false, error: "ZAP protocol not available on Firefox yet" });
710
+ break;
711
+ // --- Element selection (click-to-code) ---
712
+ case "elementSelected":
713
+ if (hanzoExtension.isConnected()) {
714
+ hanzoExtension.sendToWs({
715
+ type: "elementSelected",
716
+ data: request.data
717
+ });
718
+ }
488
719
  sendResponse({ success: true });
489
720
  break;
490
- case "auth.status": {
491
- const result = await browser.storage.local.get([STORAGE_KEYS.accessToken, STORAGE_KEYS.user]);
721
+ // --- Tab Filesystem ---
722
+ case "listTabFS": {
723
+ const allTabs = await browser.tabs.query({});
492
724
  sendResponse({
493
725
  success: true,
494
- authenticated: !!result[STORAGE_KEYS.accessToken],
495
- user: result[STORAGE_KEYS.user] || null
726
+ tabs: allTabs.map((tab) => ({
727
+ id: tab.id,
728
+ url: tab.url,
729
+ title: tab.title,
730
+ active: tab.active
731
+ }))
496
732
  });
497
733
  break;
498
734
  }
499
- case "auth.getToken": {
500
- const r = await browser.storage.local.get([STORAGE_KEYS.accessToken]);
501
- sendResponse({ success: !!r[STORAGE_KEYS.accessToken], token: r[STORAGE_KEYS.accessToken] || null });
735
+ case "readTabFS":
736
+ sendResponse({ success: false, error: "Tab filesystem read not available on Firefox" });
502
737
  break;
503
- }
504
- case "chat.listModels": {
505
- const t = await browser.storage.local.get([STORAGE_KEYS.accessToken]);
506
- if (!t[STORAGE_KEYS.accessToken]) {
507
- sendResponse({ success: false, error: "Not authenticated" });
508
- break;
509
- }
510
- const resp = await fetch(`${API_BASE}/v1/models`, { headers: { Authorization: `Bearer ${t[STORAGE_KEYS.accessToken]}` } });
511
- const data = await resp.json();
512
- sendResponse({ success: true, models: data.data || data.models || data || [] });
738
+ case "writeTabFS":
739
+ sendResponse({ success: false, error: "Tab filesystem write not available on Firefox" });
513
740
  break;
514
- }
741
+ // --- WebGPU / Local AI ---
515
742
  case "checkWebGPU": {
516
743
  try {
517
744
  if (!navigator.gpu) {
@@ -529,15 +756,19 @@
529
756
  }
530
757
  break;
531
758
  }
759
+ case "runLocalAI":
760
+ sendResponse({ success: false, error: "Local AI not available on Firefox" });
761
+ break;
532
762
  case "listAgents":
533
763
  sendResponse({ success: true, agents: [] });
534
764
  break;
535
765
  case "stopAgent":
536
- sendResponse({ success: false, error: "Agent workers are not available in Firefox background mode yet" });
766
+ sendResponse({ success: false, error: "Agent workers not available on Firefox" });
537
767
  break;
538
768
  case "launchAIWorker":
539
- sendResponse({ success: false, error: "launchAIWorker is not implemented in Firefox background mode yet" });
769
+ sendResponse({ success: false, error: "AI workers not available on Firefox" });
540
770
  break;
771
+ // --- RAG ---
541
772
  case "rag.query": {
542
773
  try {
543
774
  const config = await getRagConfig();
@@ -569,7 +800,37 @@
569
800
  }
570
801
  break;
571
802
  }
572
- // --- AI Control Overlay (forwarded to content script) ---
803
+ // --- Bridge status ---
804
+ case "bridge.status":
805
+ sendResponse({
806
+ success: true,
807
+ connected: hanzoExtension.isConnected(),
808
+ browsers: hanzoExtension.isConnected() ? ["firefox"] : []
809
+ });
810
+ break;
811
+ // --- Config ---
812
+ case "config.save":
813
+ sendResponse({ success: true });
814
+ break;
815
+ // --- In-page overlay (forwarded to content script) ---
816
+ case "page.overlay.toggle":
817
+ case "page.overlay.show":
818
+ case "page.overlay.hide":
819
+ case "page.overlay.status": {
820
+ try {
821
+ const tabId = typeof request.tabId === "number" ? request.tabId : sender.tab?.id ?? await hanzoExtension.getActiveTabId();
822
+ if (tabId === null) {
823
+ sendResponse({ success: false, error: "No target tab" });
824
+ break;
825
+ }
826
+ const response = await browser.tabs.sendMessage(tabId, { action: request.action }).catch(() => null);
827
+ sendResponse({ success: true, tabId, ...response || {} });
828
+ } catch (e) {
829
+ sendResponse({ success: false, error: e.message });
830
+ }
831
+ break;
832
+ }
833
+ // --- AI Control Overlay ---
573
834
  case "ai.control.start":
574
835
  case "ai.control.cursor":
575
836
  case "ai.control.highlight":
@@ -593,9 +854,6 @@
593
854
  break;
594
855
  }
595
856
  case "ai.control.stop":
596
- await hanzoExtension.stopControlSession();
597
- sendResponse({ success: true });
598
- break;
599
857
  case "ai.control.cancel":
600
858
  await hanzoExtension.stopControlSession();
601
859
  sendResponse({ success: true });