@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.
- package/dist/browser-extension/background-firefox.js +426 -168
- package/dist/browser-extension/background.js +38 -43
- package/dist/browser-extension/chrome/background.js +38 -43
- 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 +426 -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 +38 -43
- 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/dist/safari/Hanzo AI/Shared (App)/ViewController.swift +1 -1
- package/dist/safari/Hanzo AI/Shared (Extension)/SafariWebExtensionHandler.swift +1 -1
- package/dist/safari/Hanzo AI/iOS (App)/AppDelegate.swift +1 -1
- package/dist/safari/Hanzo AI/iOS (App)/SceneDelegate.swift +1 -1
- package/dist/safari/Hanzo AI/macOS (App)/AppDelegate.swift +1 -1
- package/hanzo-ai-chrome-v1.7.6.zip +0 -0
- package/hanzo-ai-firefox-v1.7.6.zip +0 -0
- 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,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 = (
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
491
|
-
|
|
721
|
+
// --- Tab Filesystem ---
|
|
722
|
+
case "listTabFS": {
|
|
723
|
+
const allTabs = await browser.tabs.query({});
|
|
492
724
|
sendResponse({
|
|
493
725
|
success: true,
|
|
494
|
-
|
|
495
|
-
|
|
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 "
|
|
500
|
-
|
|
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
|
-
|
|
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
|
|
766
|
+
sendResponse({ success: false, error: "Agent workers not available on Firefox" });
|
|
537
767
|
break;
|
|
538
768
|
case "launchAIWorker":
|
|
539
|
-
sendResponse({ success: false, error: "
|
|
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
|
-
// ---
|
|
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 });
|