@hanzo/browser-extension 1.7.2 → 1.7.5
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/browser-extension/background-firefox.js +414 -168
- package/dist/browser-extension/background.js +0 -17
- package/dist/browser-extension/chrome/background.js +0 -17
- package/dist/browser-extension/chrome/manifest.json +1 -1
- package/dist/browser-extension/chrome/popup.js +18 -7
- package/dist/browser-extension/chrome/sidebar.js +12 -11
- package/dist/browser-extension/firefox/background.js +414 -168
- package/dist/browser-extension/firefox/manifest.json +1 -1
- package/dist/browser-extension/firefox/popup.js +18 -7
- package/dist/browser-extension/firefox/sidebar.js +12 -11
- package/dist/browser-extension/manifest.json +1 -1
- package/dist/browser-extension/package.json +1 -1
- package/dist/browser-extension/popup.js +18 -7
- package/dist/browser-extension/safari/background.js +0 -17
- package/dist/browser-extension/safari/popup.js +18 -7
- package/dist/browser-extension/safari/sidebar.js +12 -11
- package/dist/browser-extension/sidebar.js +12 -11
- package/dist/hanzo-ai-chrome-1.7.2.zip +0 -0
- package/dist/hanzo-ai-chrome-1.7.3.zip +0 -0
- package/dist/hanzo-ai-firefox-1.7.2.zip +0 -0
- package/dist/hanzo-ai-firefox-1.7.3.zip +0 -0
- package/dist/safari/Hanzo AI/Hanzo AI.xcodeproj/project.pbxproj +295 -295
- package/package.json +1 -1
- package/src/manifest-firefox.json +1 -1
- package/src/manifest.json +1 -1
- package/src/package.json +1 -1
- package/src/chat-widget.tsx +0 -93
- package/src/popup.js +0 -192
- package/src/sidebar.js +0 -884
|
@@ -1,5 +1,241 @@
|
|
|
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_BASE = "https://hanzo.id";
|
|
31
|
+
var CLIENT_ID = "app-hanzo";
|
|
32
|
+
var SCOPES = "openid profile email";
|
|
33
|
+
function getRedirectUri() {
|
|
34
|
+
return "https://hanzo.ai/callback";
|
|
35
|
+
}
|
|
36
|
+
var STORAGE_KEYS = {
|
|
37
|
+
accessToken: "hanzo_iam_access_token",
|
|
38
|
+
refreshToken: "hanzo_iam_refresh_token",
|
|
39
|
+
idToken: "hanzo_iam_id_token",
|
|
40
|
+
expiresAt: "hanzo_iam_expires_at",
|
|
41
|
+
user: "hanzo_iam_user"
|
|
42
|
+
};
|
|
43
|
+
async function storeTokens(tokens) {
|
|
44
|
+
const data = {
|
|
45
|
+
[STORAGE_KEYS.accessToken]: tokens.access_token
|
|
46
|
+
};
|
|
47
|
+
if (tokens.refresh_token) data[STORAGE_KEYS.refreshToken] = tokens.refresh_token;
|
|
48
|
+
if (tokens.id_token) data[STORAGE_KEYS.idToken] = tokens.id_token;
|
|
49
|
+
if (tokens.expires_in) {
|
|
50
|
+
data[STORAGE_KEYS.expiresAt] = Date.now() + tokens.expires_in * 1e3;
|
|
51
|
+
}
|
|
52
|
+
return chrome.storage.local.set(data);
|
|
53
|
+
}
|
|
54
|
+
async function getStoredTokens() {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
chrome.storage.local.get(
|
|
57
|
+
[STORAGE_KEYS.accessToken, STORAGE_KEYS.refreshToken, STORAGE_KEYS.expiresAt],
|
|
58
|
+
(result) => {
|
|
59
|
+
resolve({
|
|
60
|
+
accessToken: result[STORAGE_KEYS.accessToken] || null,
|
|
61
|
+
refreshToken: result[STORAGE_KEYS.refreshToken] || null,
|
|
62
|
+
expiresAt: result[STORAGE_KEYS.expiresAt] || null
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function login() {
|
|
69
|
+
const { codeVerifier, codeChallenge } = await generatePkceChallenge();
|
|
70
|
+
const state = generateState();
|
|
71
|
+
const redirectUri = getRedirectUri();
|
|
72
|
+
const authorizeUrl = new URL(`${IAM_BASE}/login/oauth/authorize`);
|
|
73
|
+
authorizeUrl.searchParams.set("client_id", CLIENT_ID);
|
|
74
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
75
|
+
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
76
|
+
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
77
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
78
|
+
authorizeUrl.searchParams.set("scope", SCOPES);
|
|
79
|
+
authorizeUrl.searchParams.set("state", state);
|
|
80
|
+
const callbackUrl = await openAuthTab(authorizeUrl.toString(), redirectUri);
|
|
81
|
+
const url = new URL(callbackUrl);
|
|
82
|
+
const returnedState = url.searchParams.get("state");
|
|
83
|
+
if (returnedState !== state) throw new Error("State mismatch \u2014 possible CSRF");
|
|
84
|
+
const code = url.searchParams.get("code");
|
|
85
|
+
if (!code) {
|
|
86
|
+
const error = url.searchParams.get("error_description") || url.searchParams.get("error") || "No authorization code";
|
|
87
|
+
throw new Error(error);
|
|
88
|
+
}
|
|
89
|
+
const tokenResponse = await fetch(`${IAM_BASE}/oauth/token`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
92
|
+
body: new URLSearchParams({
|
|
93
|
+
grant_type: "authorization_code",
|
|
94
|
+
client_id: CLIENT_ID,
|
|
95
|
+
code,
|
|
96
|
+
redirect_uri: redirectUri,
|
|
97
|
+
code_verifier: codeVerifier
|
|
98
|
+
})
|
|
99
|
+
});
|
|
100
|
+
if (!tokenResponse.ok) {
|
|
101
|
+
const errText = await tokenResponse.text();
|
|
102
|
+
throw new Error(`Token exchange failed: ${errText}`);
|
|
103
|
+
}
|
|
104
|
+
const tokens = await tokenResponse.json();
|
|
105
|
+
await storeTokens(tokens);
|
|
106
|
+
const user = await fetchUserInfo(tokens.access_token);
|
|
107
|
+
await chrome.storage.local.set({ [STORAGE_KEYS.user]: user });
|
|
108
|
+
return user;
|
|
109
|
+
}
|
|
110
|
+
function openAuthTab(authorizeUrl, redirectUriPrefix) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
let authTabId;
|
|
113
|
+
const timeout = setTimeout(() => {
|
|
114
|
+
cleanup();
|
|
115
|
+
reject(new Error("Login timed out"));
|
|
116
|
+
}, 3e5);
|
|
117
|
+
function cleanup() {
|
|
118
|
+
clearTimeout(timeout);
|
|
119
|
+
chrome.tabs.onUpdated.removeListener(onTabUpdated);
|
|
120
|
+
chrome.tabs.onRemoved.removeListener(onTabRemoved);
|
|
121
|
+
}
|
|
122
|
+
function onTabUpdated(tabId, changeInfo) {
|
|
123
|
+
if (tabId !== authTabId || !changeInfo.url) return;
|
|
124
|
+
if (changeInfo.url.startsWith(redirectUriPrefix)) {
|
|
125
|
+
const callbackUrl = changeInfo.url;
|
|
126
|
+
cleanup();
|
|
127
|
+
chrome.tabs.remove(tabId).catch(() => {
|
|
128
|
+
});
|
|
129
|
+
resolve(callbackUrl);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function onTabRemoved(tabId) {
|
|
133
|
+
if (tabId !== authTabId) return;
|
|
134
|
+
cleanup();
|
|
135
|
+
reject(new Error("Login cancelled"));
|
|
136
|
+
}
|
|
137
|
+
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
|
138
|
+
chrome.tabs.onRemoved.addListener(onTabRemoved);
|
|
139
|
+
chrome.tabs.create({ url: authorizeUrl }, (tab) => {
|
|
140
|
+
if (chrome.runtime.lastError || !tab?.id) {
|
|
141
|
+
cleanup();
|
|
142
|
+
reject(new Error(chrome.runtime.lastError?.message || "Failed to open login tab"));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
authTabId = tab.id;
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async function logout() {
|
|
150
|
+
await chrome.storage.local.remove(Object.values(STORAGE_KEYS));
|
|
151
|
+
}
|
|
152
|
+
async function getValidAccessToken() {
|
|
153
|
+
const { accessToken, refreshToken, expiresAt } = await getStoredTokens();
|
|
154
|
+
if (!accessToken) return null;
|
|
155
|
+
if (expiresAt && Date.now() < expiresAt - 6e4) {
|
|
156
|
+
return accessToken;
|
|
157
|
+
}
|
|
158
|
+
if (refreshToken) {
|
|
159
|
+
try {
|
|
160
|
+
const refreshed = await refreshAccessToken(refreshToken);
|
|
161
|
+
return refreshed;
|
|
162
|
+
} catch {
|
|
163
|
+
await logout();
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return accessToken;
|
|
168
|
+
}
|
|
169
|
+
async function refreshAccessToken(refreshToken) {
|
|
170
|
+
const response = await fetch(`${IAM_BASE}/oauth/token`, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
173
|
+
body: new URLSearchParams({
|
|
174
|
+
grant_type: "refresh_token",
|
|
175
|
+
client_id: CLIENT_ID,
|
|
176
|
+
refresh_token: refreshToken
|
|
177
|
+
})
|
|
178
|
+
});
|
|
179
|
+
if (!response.ok) throw new Error("Token refresh failed");
|
|
180
|
+
const tokens = await response.json();
|
|
181
|
+
await storeTokens(tokens);
|
|
182
|
+
return tokens.access_token;
|
|
183
|
+
}
|
|
184
|
+
async function fetchUserInfo(accessToken) {
|
|
185
|
+
const response = await fetch(`${IAM_BASE}/oauth/userinfo`, {
|
|
186
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
187
|
+
});
|
|
188
|
+
if (!response.ok) throw new Error("Failed to fetch user info");
|
|
189
|
+
return response.json();
|
|
190
|
+
}
|
|
191
|
+
async function getUserInfo() {
|
|
192
|
+
return new Promise((resolve) => {
|
|
193
|
+
chrome.storage.local.get([STORAGE_KEYS.user], (result) => {
|
|
194
|
+
resolve(result[STORAGE_KEYS.user] || null);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async function isAuthenticated() {
|
|
199
|
+
const { accessToken } = await getStoredTokens();
|
|
200
|
+
return !!accessToken;
|
|
201
|
+
}
|
|
202
|
+
async function getAuthStatus() {
|
|
203
|
+
const [authenticated, user] = await Promise.all([
|
|
204
|
+
isAuthenticated(),
|
|
205
|
+
getUserInfo()
|
|
206
|
+
]);
|
|
207
|
+
return { authenticated, user };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/chat-client.ts
|
|
211
|
+
var API_BASE = "https://api.hanzo.ai";
|
|
212
|
+
async function listModels(token) {
|
|
213
|
+
const response = await fetch(`${API_BASE}/v1/models`, {
|
|
214
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
215
|
+
});
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
throw new Error(`Failed to list models: ${response.status}`);
|
|
218
|
+
}
|
|
219
|
+
const data = await response.json();
|
|
220
|
+
return data.data || data.models || data || [];
|
|
221
|
+
}
|
|
222
|
+
async function chatCompletion(token, options) {
|
|
223
|
+
const response = await fetch(`${API_BASE}/v1/chat/completions`, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: {
|
|
226
|
+
"Content-Type": "application/json",
|
|
227
|
+
Authorization: `Bearer ${token}`
|
|
228
|
+
},
|
|
229
|
+
body: JSON.stringify(options)
|
|
230
|
+
});
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
const text = await response.text();
|
|
233
|
+
throw new Error(`Chat API error ${response.status}: ${text}`);
|
|
234
|
+
}
|
|
235
|
+
const data = await response.json();
|
|
236
|
+
return data.choices?.[0]?.message?.content || "";
|
|
237
|
+
}
|
|
238
|
+
|
|
3
239
|
// src/background-firefox.ts
|
|
4
240
|
var controlSession = {
|
|
5
241
|
active: false,
|
|
@@ -13,7 +249,14 @@
|
|
|
13
249
|
this.cdpPort = 9223;
|
|
14
250
|
this.reconnectTimeout = 3e3;
|
|
15
251
|
console.log("[Hanzo] Firefox extension initializing...");
|
|
16
|
-
this.connect();
|
|
252
|
+
this.loadPort().then(() => this.connect());
|
|
253
|
+
}
|
|
254
|
+
async loadPort() {
|
|
255
|
+
try {
|
|
256
|
+
const result = await browser.storage.local.get(["cdpPort"]);
|
|
257
|
+
if (result.cdpPort) this.cdpPort = result.cdpPort;
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
17
260
|
}
|
|
18
261
|
connect() {
|
|
19
262
|
const url = `ws://localhost:${this.cdpPort}/cdp`;
|
|
@@ -34,16 +277,23 @@
|
|
|
34
277
|
this.send({ id: 0, error: { code: -1, message: String(e) } });
|
|
35
278
|
}
|
|
36
279
|
};
|
|
37
|
-
this.wsConnection.onerror = (
|
|
38
|
-
console.error("[Hanzo] WebSocket error:", error);
|
|
280
|
+
this.wsConnection.onerror = () => {
|
|
39
281
|
};
|
|
40
282
|
this.wsConnection.onclose = () => {
|
|
41
283
|
console.log("[Hanzo] Disconnected, reconnecting...");
|
|
42
284
|
setTimeout(() => this.connect(), this.reconnectTimeout);
|
|
43
285
|
};
|
|
44
286
|
}
|
|
287
|
+
isConnected() {
|
|
288
|
+
return this.wsConnection?.readyState === WebSocket.OPEN;
|
|
289
|
+
}
|
|
290
|
+
sendToWs(data) {
|
|
291
|
+
if (this.wsConnection?.readyState === WebSocket.OPEN) {
|
|
292
|
+
this.wsConnection.send(JSON.stringify(data));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
45
295
|
register() {
|
|
46
|
-
this.
|
|
296
|
+
this.sendToWs({
|
|
47
297
|
type: "register",
|
|
48
298
|
role: "cdp-provider",
|
|
49
299
|
capabilities: [
|
|
@@ -61,9 +311,7 @@
|
|
|
61
311
|
});
|
|
62
312
|
}
|
|
63
313
|
send(data) {
|
|
64
|
-
|
|
65
|
-
this.wsConnection.send(JSON.stringify(data));
|
|
66
|
-
}
|
|
314
|
+
this.sendToWs(data);
|
|
67
315
|
}
|
|
68
316
|
async handleCommand(message) {
|
|
69
317
|
const { id, method, params = {} } = message;
|
|
@@ -74,9 +322,6 @@
|
|
|
74
322
|
return { id, error: { code: -1, message: String(e) } };
|
|
75
323
|
}
|
|
76
324
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Wrap executeScript with a timeout to prevent blocking on GPU-heavy pages
|
|
79
|
-
*/
|
|
80
325
|
executeScriptWithTimeout(tabId, code, timeoutMs = 1e4) {
|
|
81
326
|
return Promise.race([
|
|
82
327
|
browser.tabs.executeScript(tabId, { code }),
|
|
@@ -102,17 +347,12 @@
|
|
|
102
347
|
}))
|
|
103
348
|
};
|
|
104
349
|
}
|
|
105
|
-
// Native tab property reads - never block on page content
|
|
106
350
|
case "hanzo.url": {
|
|
107
|
-
if (activeTab) {
|
|
108
|
-
return { result: { value: activeTab.url || "" } };
|
|
109
|
-
}
|
|
351
|
+
if (activeTab) return { result: { value: activeTab.url || "" } };
|
|
110
352
|
throw new Error("No active tab");
|
|
111
353
|
}
|
|
112
354
|
case "hanzo.title": {
|
|
113
|
-
if (activeTab) {
|
|
114
|
-
return { result: { value: activeTab.title || "" } };
|
|
115
|
-
}
|
|
355
|
+
if (activeTab) return { result: { value: activeTab.title || "" } };
|
|
116
356
|
throw new Error("No active tab");
|
|
117
357
|
}
|
|
118
358
|
case "hanzo.tabInfo": {
|
|
@@ -164,11 +404,7 @@
|
|
|
164
404
|
if (activeTab?.id) {
|
|
165
405
|
const expression = params.expression;
|
|
166
406
|
const timeout = params.timeout || 1e4;
|
|
167
|
-
const results = await this.executeScriptWithTimeout(
|
|
168
|
-
activeTab.id,
|
|
169
|
-
expression,
|
|
170
|
-
timeout
|
|
171
|
-
);
|
|
407
|
+
const results = await this.executeScriptWithTimeout(activeTab.id, expression, timeout);
|
|
172
408
|
return { result: { value: results[0] } };
|
|
173
409
|
}
|
|
174
410
|
throw new Error("No active tab");
|
|
@@ -202,17 +438,14 @@
|
|
|
202
438
|
const value = this.escapeValue(params.value || "");
|
|
203
439
|
this.notifyOverlay(activeTab.id, "ai.control.highlight", { selector: params.selector });
|
|
204
440
|
this.notifyOverlay(activeTab.id, "ai.control.status", { text: `Filling ${params.selector}` });
|
|
205
|
-
await this.executeScriptWithTimeout(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
`
|
|
215
|
-
);
|
|
441
|
+
await this.executeScriptWithTimeout(activeTab.id, `
|
|
442
|
+
var el = document.querySelector('${selector}');
|
|
443
|
+
if (el) {
|
|
444
|
+
el.value = '${value}';
|
|
445
|
+
el.dispatchEvent(new Event('input', {bubbles: true}));
|
|
446
|
+
el.dispatchEvent(new Event('change', {bubbles: true}));
|
|
447
|
+
}
|
|
448
|
+
`);
|
|
216
449
|
return { success: true };
|
|
217
450
|
}
|
|
218
451
|
throw new Error("No active tab or selector");
|
|
@@ -232,16 +465,13 @@
|
|
|
232
465
|
case "hanzo.type": {
|
|
233
466
|
if (activeTab?.id && params.text) {
|
|
234
467
|
const text = this.escapeValue(params.text);
|
|
235
|
-
await this.executeScriptWithTimeout(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
`
|
|
244
|
-
);
|
|
468
|
+
await this.executeScriptWithTimeout(activeTab.id, `
|
|
469
|
+
var el = document.activeElement;
|
|
470
|
+
if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) {
|
|
471
|
+
el.value += '${text}';
|
|
472
|
+
el.dispatchEvent(new Event('input', {bubbles: true}));
|
|
473
|
+
}
|
|
474
|
+
`);
|
|
245
475
|
return { success: true };
|
|
246
476
|
}
|
|
247
477
|
throw new Error("No active tab or text");
|
|
@@ -309,31 +539,6 @@
|
|
|
309
539
|
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
310
540
|
}
|
|
311
541
|
};
|
|
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
542
|
async function getRagConfig() {
|
|
338
543
|
const result = await browser.storage.local.get([
|
|
339
544
|
"hanzo_rag_endpoint",
|
|
@@ -376,9 +581,7 @@
|
|
|
376
581
|
}
|
|
377
582
|
async function queryRagEndpoint(params) {
|
|
378
583
|
if (!params.endpoint) return [];
|
|
379
|
-
const headers = {
|
|
380
|
-
"Content-Type": "application/json"
|
|
381
|
-
};
|
|
584
|
+
const headers = { "Content-Type": "application/json" };
|
|
382
585
|
if (params.apiKey) headers.Authorization = `Bearer ${params.apiKey}`;
|
|
383
586
|
const response = await fetch(params.endpoint, {
|
|
384
587
|
method: "POST",
|
|
@@ -401,117 +604,129 @@
|
|
|
401
604
|
source: snippet.source || "rag-endpoint"
|
|
402
605
|
}));
|
|
403
606
|
}
|
|
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
607
|
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
476
608
|
(async () => {
|
|
477
609
|
switch (request.action) {
|
|
610
|
+
// --- Auth (shared module — same code as Chrome) ---
|
|
478
611
|
case "auth.login":
|
|
479
612
|
try {
|
|
480
|
-
const user = await
|
|
613
|
+
const user = await login();
|
|
481
614
|
sendResponse({ success: true, user });
|
|
482
615
|
} catch (e) {
|
|
483
616
|
sendResponse({ success: false, error: e.message });
|
|
484
617
|
}
|
|
485
618
|
break;
|
|
486
619
|
case "auth.logout":
|
|
487
|
-
|
|
620
|
+
try {
|
|
621
|
+
await logout();
|
|
622
|
+
sendResponse({ success: true });
|
|
623
|
+
} catch (e) {
|
|
624
|
+
sendResponse({ success: false, error: e.message });
|
|
625
|
+
}
|
|
626
|
+
break;
|
|
627
|
+
case "auth.status":
|
|
628
|
+
try {
|
|
629
|
+
const status = await getAuthStatus();
|
|
630
|
+
sendResponse({ success: true, ...status });
|
|
631
|
+
} catch (e) {
|
|
632
|
+
sendResponse({ success: false, error: e.message });
|
|
633
|
+
}
|
|
634
|
+
break;
|
|
635
|
+
case "auth.getToken":
|
|
636
|
+
try {
|
|
637
|
+
const token = await getValidAccessToken();
|
|
638
|
+
sendResponse({ success: !!token, token });
|
|
639
|
+
} catch (e) {
|
|
640
|
+
sendResponse({ success: false, error: e.message });
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
// --- Cloud Chat (shared module) ---
|
|
644
|
+
case "chat.listModels":
|
|
645
|
+
try {
|
|
646
|
+
const token = await getValidAccessToken();
|
|
647
|
+
if (!token) {
|
|
648
|
+
sendResponse({ success: false, error: "Not authenticated" });
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
const models = await listModels(token);
|
|
652
|
+
sendResponse({ success: true, models });
|
|
653
|
+
} catch (e) {
|
|
654
|
+
sendResponse({ success: false, error: e.message });
|
|
655
|
+
}
|
|
656
|
+
break;
|
|
657
|
+
case "chat.complete":
|
|
658
|
+
try {
|
|
659
|
+
const token = await getValidAccessToken();
|
|
660
|
+
if (!token) {
|
|
661
|
+
sendResponse({ success: false, error: "Not authenticated" });
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
const model = String(request.model || "gpt-4o");
|
|
665
|
+
const messagesInput = Array.isArray(request.messages) ? request.messages : [];
|
|
666
|
+
const messages = messagesInput.filter((m) => m && typeof m.content === "string" && typeof m.role === "string").map((m) => ({
|
|
667
|
+
role: m.role === "system" || m.role === "assistant" ? m.role : "user",
|
|
668
|
+
content: String(m.content)
|
|
669
|
+
})).slice(-24);
|
|
670
|
+
if (!messages.length) {
|
|
671
|
+
sendResponse({ success: false, error: "messages are required" });
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
const content = await chatCompletion(token, {
|
|
675
|
+
model,
|
|
676
|
+
messages,
|
|
677
|
+
temperature: typeof request.temperature === "number" ? request.temperature : void 0,
|
|
678
|
+
max_tokens: typeof request.max_tokens === "number" ? request.max_tokens : void 0
|
|
679
|
+
});
|
|
680
|
+
sendResponse({ success: true, content });
|
|
681
|
+
} catch (e) {
|
|
682
|
+
sendResponse({ success: false, error: e.message });
|
|
683
|
+
}
|
|
684
|
+
break;
|
|
685
|
+
// --- ZAP Protocol (not available on Firefox — return proper status) ---
|
|
686
|
+
case "zap.status":
|
|
687
|
+
sendResponse({ success: true, zap: { connected: false, mcps: [], extensionId: "firefox" } });
|
|
688
|
+
break;
|
|
689
|
+
case "zap.discover":
|
|
690
|
+
sendResponse({ success: true, mcps: [] });
|
|
691
|
+
break;
|
|
692
|
+
case "zap.listTools":
|
|
693
|
+
sendResponse({ success: true, tools: [] });
|
|
694
|
+
break;
|
|
695
|
+
case "zap.connect":
|
|
696
|
+
case "zap.callTool":
|
|
697
|
+
sendResponse({ success: false, error: "ZAP protocol not available on Firefox yet" });
|
|
698
|
+
break;
|
|
699
|
+
// --- Element selection (click-to-code) ---
|
|
700
|
+
case "elementSelected":
|
|
701
|
+
if (hanzoExtension.isConnected()) {
|
|
702
|
+
hanzoExtension.sendToWs({
|
|
703
|
+
type: "elementSelected",
|
|
704
|
+
data: request.data
|
|
705
|
+
});
|
|
706
|
+
}
|
|
488
707
|
sendResponse({ success: true });
|
|
489
708
|
break;
|
|
490
|
-
|
|
491
|
-
|
|
709
|
+
// --- Tab Filesystem ---
|
|
710
|
+
case "listTabFS": {
|
|
711
|
+
const allTabs = await browser.tabs.query({});
|
|
492
712
|
sendResponse({
|
|
493
713
|
success: true,
|
|
494
|
-
|
|
495
|
-
|
|
714
|
+
tabs: allTabs.map((tab) => ({
|
|
715
|
+
id: tab.id,
|
|
716
|
+
url: tab.url,
|
|
717
|
+
title: tab.title,
|
|
718
|
+
active: tab.active
|
|
719
|
+
}))
|
|
496
720
|
});
|
|
497
721
|
break;
|
|
498
722
|
}
|
|
499
|
-
case "
|
|
500
|
-
|
|
501
|
-
sendResponse({ success: !!r[STORAGE_KEYS.accessToken], token: r[STORAGE_KEYS.accessToken] || null });
|
|
723
|
+
case "readTabFS":
|
|
724
|
+
sendResponse({ success: false, error: "Tab filesystem read not available on Firefox" });
|
|
502
725
|
break;
|
|
503
|
-
|
|
504
|
-
|
|
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 || [] });
|
|
726
|
+
case "writeTabFS":
|
|
727
|
+
sendResponse({ success: false, error: "Tab filesystem write not available on Firefox" });
|
|
513
728
|
break;
|
|
514
|
-
|
|
729
|
+
// --- WebGPU / Local AI ---
|
|
515
730
|
case "checkWebGPU": {
|
|
516
731
|
try {
|
|
517
732
|
if (!navigator.gpu) {
|
|
@@ -529,15 +744,19 @@
|
|
|
529
744
|
}
|
|
530
745
|
break;
|
|
531
746
|
}
|
|
747
|
+
case "runLocalAI":
|
|
748
|
+
sendResponse({ success: false, error: "Local AI not available on Firefox" });
|
|
749
|
+
break;
|
|
532
750
|
case "listAgents":
|
|
533
751
|
sendResponse({ success: true, agents: [] });
|
|
534
752
|
break;
|
|
535
753
|
case "stopAgent":
|
|
536
|
-
sendResponse({ success: false, error: "Agent workers
|
|
754
|
+
sendResponse({ success: false, error: "Agent workers not available on Firefox" });
|
|
537
755
|
break;
|
|
538
756
|
case "launchAIWorker":
|
|
539
|
-
sendResponse({ success: false, error: "
|
|
757
|
+
sendResponse({ success: false, error: "AI workers not available on Firefox" });
|
|
540
758
|
break;
|
|
759
|
+
// --- RAG ---
|
|
541
760
|
case "rag.query": {
|
|
542
761
|
try {
|
|
543
762
|
const config = await getRagConfig();
|
|
@@ -569,7 +788,37 @@
|
|
|
569
788
|
}
|
|
570
789
|
break;
|
|
571
790
|
}
|
|
572
|
-
// ---
|
|
791
|
+
// --- Bridge status ---
|
|
792
|
+
case "bridge.status":
|
|
793
|
+
sendResponse({
|
|
794
|
+
success: true,
|
|
795
|
+
connected: hanzoExtension.isConnected(),
|
|
796
|
+
browsers: hanzoExtension.isConnected() ? ["firefox"] : []
|
|
797
|
+
});
|
|
798
|
+
break;
|
|
799
|
+
// --- Config ---
|
|
800
|
+
case "config.save":
|
|
801
|
+
sendResponse({ success: true });
|
|
802
|
+
break;
|
|
803
|
+
// --- In-page overlay (forwarded to content script) ---
|
|
804
|
+
case "page.overlay.toggle":
|
|
805
|
+
case "page.overlay.show":
|
|
806
|
+
case "page.overlay.hide":
|
|
807
|
+
case "page.overlay.status": {
|
|
808
|
+
try {
|
|
809
|
+
const tabId = typeof request.tabId === "number" ? request.tabId : sender.tab?.id ?? await hanzoExtension.getActiveTabId();
|
|
810
|
+
if (tabId === null) {
|
|
811
|
+
sendResponse({ success: false, error: "No target tab" });
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
const response = await browser.tabs.sendMessage(tabId, { action: request.action }).catch(() => null);
|
|
815
|
+
sendResponse({ success: true, tabId, ...response || {} });
|
|
816
|
+
} catch (e) {
|
|
817
|
+
sendResponse({ success: false, error: e.message });
|
|
818
|
+
}
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
// --- AI Control Overlay ---
|
|
573
822
|
case "ai.control.start":
|
|
574
823
|
case "ai.control.cursor":
|
|
575
824
|
case "ai.control.highlight":
|
|
@@ -593,9 +842,6 @@
|
|
|
593
842
|
break;
|
|
594
843
|
}
|
|
595
844
|
case "ai.control.stop":
|
|
596
|
-
await hanzoExtension.stopControlSession();
|
|
597
|
-
sendResponse({ success: true });
|
|
598
|
-
break;
|
|
599
845
|
case "ai.control.cancel":
|
|
600
846
|
await hanzoExtension.stopControlSession();
|
|
601
847
|
sendResponse({ success: true });
|