@quanta-intellect/vessel-browser 0.1.18 → 0.1.19
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/README.md +17 -0
- package/out/main/index.js +922 -291
- package/out/preload/index.js +13 -0
- package/out/renderer/assets/{index-DAaJOss-.js → index-CKOT_IZt.js} +947 -209
- package/out/renderer/assets/{index-DMd-y6tm.css → index-DwRZftNk.css} +110 -0
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
package/out/main/index.js
CHANGED
|
@@ -4,11 +4,11 @@ const fs$1 = require("node:fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const crypto = require("crypto");
|
|
7
|
+
const crypto$1 = require("node:crypto");
|
|
8
|
+
const path$1 = require("node:path");
|
|
7
9
|
const Anthropic = require("@anthropic-ai/sdk");
|
|
8
10
|
const OpenAI = require("openai");
|
|
9
11
|
const zod = require("zod");
|
|
10
|
-
const path$1 = require("node:path");
|
|
11
|
-
const crypto$1 = require("node:crypto");
|
|
12
12
|
const http = require("node:http");
|
|
13
13
|
const os = require("node:os");
|
|
14
14
|
const mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
@@ -1228,7 +1228,7 @@ function subscribe$1(listener) {
|
|
|
1228
1228
|
listeners$1.delete(listener);
|
|
1229
1229
|
};
|
|
1230
1230
|
}
|
|
1231
|
-
function addEntry(url, title) {
|
|
1231
|
+
function addEntry$1(url, title) {
|
|
1232
1232
|
if (!url || url === "about:blank") return;
|
|
1233
1233
|
load$1();
|
|
1234
1234
|
const last = state$2.entries[0];
|
|
@@ -1991,7 +1991,7 @@ class TabManager {
|
|
|
1991
1991
|
},
|
|
1992
1992
|
onPageLoad: (pageUrl, wc) => {
|
|
1993
1993
|
this.reapplyHighlights(pageUrl, wc);
|
|
1994
|
-
addEntry(pageUrl, wc.getTitle());
|
|
1994
|
+
addEntry$1(pageUrl, wc.getTitle());
|
|
1995
1995
|
},
|
|
1996
1996
|
onHighlightSelection: (wc) => this.captureHighlightFromPage(wc),
|
|
1997
1997
|
onHighlightRemove: (url2, text) => this.removeHighlightByText(url2, text),
|
|
@@ -2349,6 +2349,12 @@ const Channels = {
|
|
|
2349
2349
|
PREMIUM_PORTAL: "premium:portal",
|
|
2350
2350
|
PREMIUM_RESET: "premium:reset",
|
|
2351
2351
|
PREMIUM_UPDATE: "premium:update",
|
|
2352
|
+
// Agent Credential Vault
|
|
2353
|
+
VAULT_LIST: "vault:list",
|
|
2354
|
+
VAULT_ADD: "vault:add",
|
|
2355
|
+
VAULT_UPDATE: "vault:update",
|
|
2356
|
+
VAULT_REMOVE: "vault:remove",
|
|
2357
|
+
VAULT_AUDIT_LOG: "vault:audit-log",
|
|
2352
2358
|
// Window controls
|
|
2353
2359
|
WINDOW_MINIMIZE: "window:minimize",
|
|
2354
2360
|
WINDOW_MAXIMIZE: "window:maximize",
|
|
@@ -3184,6 +3190,309 @@ function addIfPresent(target, key, value) {
|
|
|
3184
3190
|
target[key] = value;
|
|
3185
3191
|
}
|
|
3186
3192
|
}
|
|
3193
|
+
const VERIFICATION_API = process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
|
|
3194
|
+
const FREE_TOOL_ITERATION_LIMIT = 50;
|
|
3195
|
+
const REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3196
|
+
const OFFLINE_GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
3197
|
+
const PREMIUM_TOOLS = /* @__PURE__ */ new Set([
|
|
3198
|
+
"screenshot",
|
|
3199
|
+
"save_session",
|
|
3200
|
+
"load_session",
|
|
3201
|
+
"list_sessions",
|
|
3202
|
+
"delete_session",
|
|
3203
|
+
"flow_start",
|
|
3204
|
+
"flow_advance",
|
|
3205
|
+
"flow_status",
|
|
3206
|
+
"flow_end",
|
|
3207
|
+
"metrics",
|
|
3208
|
+
"extract_table",
|
|
3209
|
+
"vault_login",
|
|
3210
|
+
"vault_status",
|
|
3211
|
+
"vault_totp"
|
|
3212
|
+
]);
|
|
3213
|
+
function isPremium() {
|
|
3214
|
+
const { premium } = loadSettings();
|
|
3215
|
+
if (premium.status === "active" || premium.status === "trialing") {
|
|
3216
|
+
return true;
|
|
3217
|
+
}
|
|
3218
|
+
if (premium.validatedAt && premium.status !== "free") {
|
|
3219
|
+
const lastValidated = new Date(premium.validatedAt).getTime();
|
|
3220
|
+
if (Date.now() - lastValidated < OFFLINE_GRACE_PERIOD_MS) {
|
|
3221
|
+
return true;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
return false;
|
|
3225
|
+
}
|
|
3226
|
+
function getPremiumState() {
|
|
3227
|
+
return { ...loadSettings().premium };
|
|
3228
|
+
}
|
|
3229
|
+
function getEffectiveMaxIterations() {
|
|
3230
|
+
if (isPremium()) {
|
|
3231
|
+
return loadSettings().maxToolIterations || 200;
|
|
3232
|
+
}
|
|
3233
|
+
return FREE_TOOL_ITERATION_LIMIT;
|
|
3234
|
+
}
|
|
3235
|
+
function resetPremium() {
|
|
3236
|
+
const fresh = {
|
|
3237
|
+
status: "free",
|
|
3238
|
+
customerId: "",
|
|
3239
|
+
email: "",
|
|
3240
|
+
validatedAt: "",
|
|
3241
|
+
expiresAt: ""
|
|
3242
|
+
};
|
|
3243
|
+
setSetting("premium", fresh);
|
|
3244
|
+
return fresh;
|
|
3245
|
+
}
|
|
3246
|
+
function isToolGated(toolName) {
|
|
3247
|
+
return PREMIUM_TOOLS.has(toolName) && !isPremium();
|
|
3248
|
+
}
|
|
3249
|
+
async function getCheckoutUrl(email) {
|
|
3250
|
+
try {
|
|
3251
|
+
const params = new URLSearchParams();
|
|
3252
|
+
if (email) params.set("email", email);
|
|
3253
|
+
const res = await fetch(`${VERIFICATION_API}/checkout?${params}`, {
|
|
3254
|
+
method: "POST",
|
|
3255
|
+
headers: { "Content-Type": "application/json" }
|
|
3256
|
+
});
|
|
3257
|
+
if (!res.ok) {
|
|
3258
|
+
const body = await res.text();
|
|
3259
|
+
return { ok: false, error: body || `HTTP ${res.status}` };
|
|
3260
|
+
}
|
|
3261
|
+
const { url } = await res.json();
|
|
3262
|
+
return { ok: true, url };
|
|
3263
|
+
} catch (err) {
|
|
3264
|
+
return {
|
|
3265
|
+
ok: false,
|
|
3266
|
+
error: err instanceof Error ? err.message : "Failed to create checkout"
|
|
3267
|
+
};
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
async function getPortalUrl() {
|
|
3271
|
+
const { premium } = loadSettings();
|
|
3272
|
+
if (!premium.customerId) {
|
|
3273
|
+
return { ok: false, error: "No active subscription" };
|
|
3274
|
+
}
|
|
3275
|
+
try {
|
|
3276
|
+
const res = await fetch(`${VERIFICATION_API}/portal`, {
|
|
3277
|
+
method: "POST",
|
|
3278
|
+
headers: { "Content-Type": "application/json" },
|
|
3279
|
+
body: JSON.stringify({ customerId: premium.customerId })
|
|
3280
|
+
});
|
|
3281
|
+
if (!res.ok) {
|
|
3282
|
+
return { ok: false, error: `HTTP ${res.status}` };
|
|
3283
|
+
}
|
|
3284
|
+
const { url } = await res.json();
|
|
3285
|
+
return { ok: true, url };
|
|
3286
|
+
} catch (err) {
|
|
3287
|
+
return {
|
|
3288
|
+
ok: false,
|
|
3289
|
+
error: err instanceof Error ? err.message : "Failed to get portal URL"
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
async function verifySubscription(emailOrCustomerId) {
|
|
3294
|
+
const current = loadSettings().premium;
|
|
3295
|
+
const identifier = emailOrCustomerId || current.customerId || current.email;
|
|
3296
|
+
if (!identifier) {
|
|
3297
|
+
return current;
|
|
3298
|
+
}
|
|
3299
|
+
try {
|
|
3300
|
+
const res = await fetch(`${VERIFICATION_API}/verify`, {
|
|
3301
|
+
method: "POST",
|
|
3302
|
+
headers: { "Content-Type": "application/json" },
|
|
3303
|
+
body: JSON.stringify({ identifier })
|
|
3304
|
+
});
|
|
3305
|
+
if (!res.ok) {
|
|
3306
|
+
console.warn("[Vessel Premium] Verification API returned", res.status);
|
|
3307
|
+
return current;
|
|
3308
|
+
}
|
|
3309
|
+
const data = await res.json();
|
|
3310
|
+
const updated = {
|
|
3311
|
+
status: data.status,
|
|
3312
|
+
customerId: data.customerId,
|
|
3313
|
+
email: data.email,
|
|
3314
|
+
validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3315
|
+
expiresAt: data.expiresAt
|
|
3316
|
+
};
|
|
3317
|
+
setSetting("premium", updated);
|
|
3318
|
+
return updated;
|
|
3319
|
+
} catch (err) {
|
|
3320
|
+
console.warn("[Vessel Premium] Verification failed:", err);
|
|
3321
|
+
return current;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
async function activateWithEmail(email) {
|
|
3325
|
+
if (!email.trim()) {
|
|
3326
|
+
return { ok: false, state: getPremiumState(), error: "Email is required" };
|
|
3327
|
+
}
|
|
3328
|
+
const state2 = await verifySubscription(email.trim());
|
|
3329
|
+
if (state2.status === "active" || state2.status === "trialing") {
|
|
3330
|
+
return { ok: true, state: state2 };
|
|
3331
|
+
}
|
|
3332
|
+
return {
|
|
3333
|
+
ok: false,
|
|
3334
|
+
state: state2,
|
|
3335
|
+
error: state2.status === "canceled" ? "Subscription is canceled. Resubscribe to continue." : state2.status === "past_due" ? "Subscription payment is past due. Update your payment method." : "No active subscription found for this email."
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
let revalidationTimer = null;
|
|
3339
|
+
function startBackgroundRevalidation() {
|
|
3340
|
+
if (revalidationTimer) return;
|
|
3341
|
+
const { premium } = loadSettings();
|
|
3342
|
+
if (premium.customerId || premium.email) {
|
|
3343
|
+
const lastValidated = premium.validatedAt ? new Date(premium.validatedAt).getTime() : 0;
|
|
3344
|
+
if (Date.now() - lastValidated > REVALIDATION_INTERVAL_MS) {
|
|
3345
|
+
void verifySubscription();
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
revalidationTimer = setInterval(() => {
|
|
3349
|
+
const { premium: p } = loadSettings();
|
|
3350
|
+
if (p.customerId || p.email) {
|
|
3351
|
+
void verifySubscription();
|
|
3352
|
+
}
|
|
3353
|
+
}, REVALIDATION_INTERVAL_MS);
|
|
3354
|
+
}
|
|
3355
|
+
function stopBackgroundRevalidation() {
|
|
3356
|
+
if (revalidationTimer) {
|
|
3357
|
+
clearInterval(revalidationTimer);
|
|
3358
|
+
revalidationTimer = null;
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || "phc_OMeM3P5cxJwl14lOKxYad0Yre52xvjNfkLEFnPtXyM";
|
|
3362
|
+
const POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
|
|
3363
|
+
const BATCH_INTERVAL_MS = 6e4;
|
|
3364
|
+
const MAX_BATCH_SIZE = 50;
|
|
3365
|
+
function getDeviceIdPath() {
|
|
3366
|
+
return path.join(electron.app.getPath("userData"), ".vessel-device-id");
|
|
3367
|
+
}
|
|
3368
|
+
let deviceId = null;
|
|
3369
|
+
function getDeviceId() {
|
|
3370
|
+
if (deviceId) return deviceId;
|
|
3371
|
+
const idPath = getDeviceIdPath();
|
|
3372
|
+
try {
|
|
3373
|
+
deviceId = fs.readFileSync(idPath, "utf-8").trim();
|
|
3374
|
+
if (deviceId) return deviceId;
|
|
3375
|
+
} catch {
|
|
3376
|
+
}
|
|
3377
|
+
deviceId = crypto.randomUUID();
|
|
3378
|
+
try {
|
|
3379
|
+
fs.mkdirSync(path.dirname(idPath), { recursive: true });
|
|
3380
|
+
fs.writeFileSync(idPath, deviceId, "utf-8");
|
|
3381
|
+
} catch {
|
|
3382
|
+
}
|
|
3383
|
+
return deviceId;
|
|
3384
|
+
}
|
|
3385
|
+
let eventQueue = [];
|
|
3386
|
+
let flushTimer = null;
|
|
3387
|
+
let sessionStartedAt = null;
|
|
3388
|
+
function isEnabled() {
|
|
3389
|
+
if (POSTHOG_API_KEY === "YOUR_POSTHOG_KEY_HERE") return false;
|
|
3390
|
+
return loadSettings().telemetryEnabled !== false;
|
|
3391
|
+
}
|
|
3392
|
+
function trackEvent(event, properties = {}) {
|
|
3393
|
+
if (!isEnabled()) return;
|
|
3394
|
+
eventQueue.push({
|
|
3395
|
+
event,
|
|
3396
|
+
properties: {
|
|
3397
|
+
...properties,
|
|
3398
|
+
premium_status: isPremium() ? "premium" : "free",
|
|
3399
|
+
app_version: electron.app.getVersion(),
|
|
3400
|
+
platform: process.platform,
|
|
3401
|
+
arch: process.arch
|
|
3402
|
+
},
|
|
3403
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3404
|
+
});
|
|
3405
|
+
if (eventQueue.length >= MAX_BATCH_SIZE) {
|
|
3406
|
+
void flush();
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
function trackToolCall(toolName, pageType) {
|
|
3410
|
+
trackEvent("tool_called", {
|
|
3411
|
+
tool_name: toolName,
|
|
3412
|
+
page_type: "unknown"
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
function trackProviderConfigured(providerId) {
|
|
3416
|
+
trackEvent("provider_configured", {
|
|
3417
|
+
provider_id: providerId
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
function trackSettingChanged(key) {
|
|
3421
|
+
trackEvent("setting_changed", { setting_key: key });
|
|
3422
|
+
}
|
|
3423
|
+
function trackApprovalModeChanged(mode) {
|
|
3424
|
+
trackEvent("approval_mode_changed", { mode });
|
|
3425
|
+
}
|
|
3426
|
+
function trackBookmarkAction(action) {
|
|
3427
|
+
trackEvent("bookmark_action", { action });
|
|
3428
|
+
}
|
|
3429
|
+
function trackVaultAction(action) {
|
|
3430
|
+
trackEvent("vault_action", { action });
|
|
3431
|
+
}
|
|
3432
|
+
function trackExtractionFailed(domain, reason) {
|
|
3433
|
+
trackEvent("extraction_failed", { domain, reason });
|
|
3434
|
+
}
|
|
3435
|
+
function trackPremiumFunnel(step, context) {
|
|
3436
|
+
trackEvent("premium_funnel", { step, ...context });
|
|
3437
|
+
}
|
|
3438
|
+
function startTelemetry() {
|
|
3439
|
+
if (!isEnabled()) return;
|
|
3440
|
+
sessionStartedAt = Date.now();
|
|
3441
|
+
trackEvent("app_launched", {
|
|
3442
|
+
electron_version: process.versions.electron,
|
|
3443
|
+
chrome_version: process.versions.chrome
|
|
3444
|
+
});
|
|
3445
|
+
flushTimer = setInterval(() => {
|
|
3446
|
+
void flush();
|
|
3447
|
+
}, BATCH_INTERVAL_MS);
|
|
3448
|
+
}
|
|
3449
|
+
function stopTelemetry() {
|
|
3450
|
+
if (sessionStartedAt) {
|
|
3451
|
+
const durationMinutes = Math.round(
|
|
3452
|
+
(Date.now() - sessionStartedAt) / 6e4
|
|
3453
|
+
);
|
|
3454
|
+
trackEvent("app_session_ended", {
|
|
3455
|
+
duration_minutes: durationMinutes
|
|
3456
|
+
});
|
|
3457
|
+
sessionStartedAt = null;
|
|
3458
|
+
}
|
|
3459
|
+
if (flushTimer) {
|
|
3460
|
+
clearInterval(flushTimer);
|
|
3461
|
+
flushTimer = null;
|
|
3462
|
+
}
|
|
3463
|
+
void flush();
|
|
3464
|
+
}
|
|
3465
|
+
async function flush() {
|
|
3466
|
+
if (eventQueue.length === 0) return;
|
|
3467
|
+
if (!isEnabled()) {
|
|
3468
|
+
eventQueue = [];
|
|
3469
|
+
return;
|
|
3470
|
+
}
|
|
3471
|
+
const batch = eventQueue.splice(0);
|
|
3472
|
+
const distinctId = getDeviceId();
|
|
3473
|
+
const payload = {
|
|
3474
|
+
api_key: POSTHOG_API_KEY,
|
|
3475
|
+
batch: batch.map((e) => ({
|
|
3476
|
+
event: e.event,
|
|
3477
|
+
properties: {
|
|
3478
|
+
distinct_id: distinctId,
|
|
3479
|
+
...e.properties
|
|
3480
|
+
},
|
|
3481
|
+
timestamp: e.timestamp
|
|
3482
|
+
}))
|
|
3483
|
+
};
|
|
3484
|
+
try {
|
|
3485
|
+
await fetch(`${POSTHOG_HOST}/batch`, {
|
|
3486
|
+
method: "POST",
|
|
3487
|
+
headers: { "Content-Type": "application/json" },
|
|
3488
|
+
body: JSON.stringify(payload)
|
|
3489
|
+
});
|
|
3490
|
+
} catch {
|
|
3491
|
+
if (eventQueue.length < MAX_BATCH_SIZE * 2) {
|
|
3492
|
+
eventQueue.unshift(...batch);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3187
3496
|
const EMPTY_PAGE_CONTENT = {
|
|
3188
3497
|
title: "",
|
|
3189
3498
|
content: "",
|
|
@@ -3229,7 +3538,7 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
|
|
|
3229
3538
|
(function() {
|
|
3230
3539
|
// Time budget: stop expensive DOM traversals after this many ms so heavy
|
|
3231
3540
|
// pages (Newegg, Wikipedia, etc.) don't stall the agent for 30-60s+.
|
|
3232
|
-
var BUDGET_MS =
|
|
3541
|
+
var BUDGET_MS = 8000;
|
|
3233
3542
|
var _budgetStart = performance.now();
|
|
3234
3543
|
function withinBudget() {
|
|
3235
3544
|
return (performance.now() - _budgetStart) < BUDGET_MS;
|
|
@@ -4088,7 +4397,7 @@ const SAFE_EXTRACTION_SCRIPT = String.raw`
|
|
|
4088
4397
|
function delay(ms) {
|
|
4089
4398
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4090
4399
|
}
|
|
4091
|
-
const EXECUTE_SCRIPT_TIMEOUT_MS =
|
|
4400
|
+
const EXECUTE_SCRIPT_TIMEOUT_MS = 3e3;
|
|
4092
4401
|
async function waitForDomReady(webContents, timeoutMs = 1500) {
|
|
4093
4402
|
const deadline = Date.now() + timeoutMs;
|
|
4094
4403
|
while (Date.now() < deadline) {
|
|
@@ -4204,7 +4513,25 @@ function mergePageContent(candidates, webContents) {
|
|
|
4204
4513
|
url: mergedBase.url || webContents.getURL() || ""
|
|
4205
4514
|
};
|
|
4206
4515
|
}
|
|
4207
|
-
const
|
|
4516
|
+
const EXTRACT_TIMEOUT_BASE_MS = 12e3;
|
|
4517
|
+
const EXTRACT_TIMEOUT_MAX_MS = 2e4;
|
|
4518
|
+
async function estimateExtractionTimeout(webContents) {
|
|
4519
|
+
try {
|
|
4520
|
+
const elementCount = await executeScript(
|
|
4521
|
+
webContents,
|
|
4522
|
+
`(function() { try { return document.querySelectorAll('*').length; } catch { return 0; } })()`
|
|
4523
|
+
);
|
|
4524
|
+
if (typeof elementCount === "number" && elementCount > 5e3) {
|
|
4525
|
+
const extra = Math.min(
|
|
4526
|
+
EXTRACT_TIMEOUT_MAX_MS - EXTRACT_TIMEOUT_BASE_MS,
|
|
4527
|
+
Math.ceil((elementCount - 5e3) / 2e3) * 1e3
|
|
4528
|
+
);
|
|
4529
|
+
return EXTRACT_TIMEOUT_BASE_MS + extra;
|
|
4530
|
+
}
|
|
4531
|
+
} catch {
|
|
4532
|
+
}
|
|
4533
|
+
return EXTRACT_TIMEOUT_BASE_MS;
|
|
4534
|
+
}
|
|
4208
4535
|
async function extractContentInner(webContents) {
|
|
4209
4536
|
await waitForDomReady(webContents);
|
|
4210
4537
|
const [preloadResult, directResult, safeResult] = await Promise.all([
|
|
@@ -4219,20 +4546,29 @@ async function extractContentInner(webContents) {
|
|
|
4219
4546
|
}
|
|
4220
4547
|
async function extractContent(webContents) {
|
|
4221
4548
|
try {
|
|
4549
|
+
const timeoutMs = await estimateExtractionTimeout(webContents);
|
|
4222
4550
|
return await Promise.race([
|
|
4223
4551
|
extractContentInner(webContents),
|
|
4224
4552
|
new Promise(
|
|
4225
4553
|
(_, reject) => setTimeout(
|
|
4226
4554
|
() => reject(new Error("extractContent timeout")),
|
|
4227
|
-
|
|
4555
|
+
timeoutMs
|
|
4228
4556
|
)
|
|
4229
4557
|
)
|
|
4230
4558
|
]);
|
|
4231
|
-
} catch {
|
|
4559
|
+
} catch (err) {
|
|
4560
|
+
const url = webContents.getURL() || "";
|
|
4561
|
+
let domain = "unknown";
|
|
4562
|
+
try {
|
|
4563
|
+
domain = new URL(url).hostname;
|
|
4564
|
+
} catch {
|
|
4565
|
+
}
|
|
4566
|
+
const reason = err instanceof Error ? err.message : "unknown";
|
|
4567
|
+
trackExtractionFailed(domain, reason);
|
|
4232
4568
|
return {
|
|
4233
4569
|
...EMPTY_PAGE_CONTENT,
|
|
4234
4570
|
title: webContents.getTitle() || "",
|
|
4235
|
-
url
|
|
4571
|
+
url
|
|
4236
4572
|
};
|
|
4237
4573
|
}
|
|
4238
4574
|
}
|
|
@@ -4399,298 +4735,217 @@ function setMcpHealth(update) {
|
|
|
4399
4735
|
if ("activePort" in update) {
|
|
4400
4736
|
state$1.mcp.activePort = update.activePort ?? null;
|
|
4401
4737
|
}
|
|
4402
|
-
if ("endpoint" in update) {
|
|
4403
|
-
state$1.mcp.endpoint = update.endpoint ?? null;
|
|
4404
|
-
}
|
|
4405
|
-
const prevStatus = state$1.mcp.status;
|
|
4406
|
-
state$1.mcp.status = update.status;
|
|
4407
|
-
state$1.mcp.message = update.message;
|
|
4408
|
-
if (prevStatus !== state$1.mcp.status) {
|
|
4409
|
-
for (const listener of mcpStatusChangeListeners) {
|
|
4410
|
-
listener(state$1.mcp.status);
|
|
4411
|
-
}
|
|
4412
|
-
}
|
|
4413
|
-
}
|
|
4414
|
-
const VERIFICATION_API = process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
|
|
4415
|
-
const FREE_TOOL_ITERATION_LIMIT = 50;
|
|
4416
|
-
const REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
4417
|
-
const OFFLINE_GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4418
|
-
const PREMIUM_TOOLS = /* @__PURE__ */ new Set([
|
|
4419
|
-
"screenshot",
|
|
4420
|
-
"save_session",
|
|
4421
|
-
"load_session",
|
|
4422
|
-
"list_sessions",
|
|
4423
|
-
"delete_session",
|
|
4424
|
-
"flow_start",
|
|
4425
|
-
"flow_advance",
|
|
4426
|
-
"flow_status",
|
|
4427
|
-
"flow_end",
|
|
4428
|
-
"metrics",
|
|
4429
|
-
"extract_table"
|
|
4430
|
-
]);
|
|
4431
|
-
function isPremium() {
|
|
4432
|
-
const { premium } = loadSettings();
|
|
4433
|
-
if (premium.status === "active" || premium.status === "trialing") {
|
|
4434
|
-
return true;
|
|
4435
|
-
}
|
|
4436
|
-
if (premium.validatedAt && premium.status !== "free") {
|
|
4437
|
-
const lastValidated = new Date(premium.validatedAt).getTime();
|
|
4438
|
-
if (Date.now() - lastValidated < OFFLINE_GRACE_PERIOD_MS) {
|
|
4439
|
-
return true;
|
|
4440
|
-
}
|
|
4441
|
-
}
|
|
4442
|
-
return false;
|
|
4443
|
-
}
|
|
4444
|
-
function getPremiumState() {
|
|
4445
|
-
return { ...loadSettings().premium };
|
|
4446
|
-
}
|
|
4447
|
-
function getEffectiveMaxIterations() {
|
|
4448
|
-
if (isPremium()) {
|
|
4449
|
-
return loadSettings().maxToolIterations || 200;
|
|
4450
|
-
}
|
|
4451
|
-
return FREE_TOOL_ITERATION_LIMIT;
|
|
4452
|
-
}
|
|
4453
|
-
function resetPremium() {
|
|
4454
|
-
const fresh = {
|
|
4455
|
-
status: "free",
|
|
4456
|
-
customerId: "",
|
|
4457
|
-
email: "",
|
|
4458
|
-
validatedAt: "",
|
|
4459
|
-
expiresAt: ""
|
|
4460
|
-
};
|
|
4461
|
-
setSetting("premium", fresh);
|
|
4462
|
-
return fresh;
|
|
4463
|
-
}
|
|
4464
|
-
function isToolGated(toolName) {
|
|
4465
|
-
return PREMIUM_TOOLS.has(toolName) && !isPremium();
|
|
4466
|
-
}
|
|
4467
|
-
async function getCheckoutUrl(email) {
|
|
4468
|
-
try {
|
|
4469
|
-
const params = new URLSearchParams();
|
|
4470
|
-
if (email) params.set("email", email);
|
|
4471
|
-
const res = await fetch(`${VERIFICATION_API}/checkout?${params}`, {
|
|
4472
|
-
method: "POST",
|
|
4473
|
-
headers: { "Content-Type": "application/json" }
|
|
4474
|
-
});
|
|
4475
|
-
if (!res.ok) {
|
|
4476
|
-
const body = await res.text();
|
|
4477
|
-
return { ok: false, error: body || `HTTP ${res.status}` };
|
|
4478
|
-
}
|
|
4479
|
-
const { url } = await res.json();
|
|
4480
|
-
return { ok: true, url };
|
|
4481
|
-
} catch (err) {
|
|
4482
|
-
return {
|
|
4483
|
-
ok: false,
|
|
4484
|
-
error: err instanceof Error ? err.message : "Failed to create checkout"
|
|
4485
|
-
};
|
|
4486
|
-
}
|
|
4487
|
-
}
|
|
4488
|
-
async function getPortalUrl() {
|
|
4489
|
-
const { premium } = loadSettings();
|
|
4490
|
-
if (!premium.customerId) {
|
|
4491
|
-
return { ok: false, error: "No active subscription" };
|
|
4492
|
-
}
|
|
4493
|
-
try {
|
|
4494
|
-
const res = await fetch(`${VERIFICATION_API}/portal`, {
|
|
4495
|
-
method: "POST",
|
|
4496
|
-
headers: { "Content-Type": "application/json" },
|
|
4497
|
-
body: JSON.stringify({ customerId: premium.customerId })
|
|
4498
|
-
});
|
|
4499
|
-
if (!res.ok) {
|
|
4500
|
-
return { ok: false, error: `HTTP ${res.status}` };
|
|
4501
|
-
}
|
|
4502
|
-
const { url } = await res.json();
|
|
4503
|
-
return { ok: true, url };
|
|
4504
|
-
} catch (err) {
|
|
4505
|
-
return {
|
|
4506
|
-
ok: false,
|
|
4507
|
-
error: err instanceof Error ? err.message : "Failed to get portal URL"
|
|
4508
|
-
};
|
|
4509
|
-
}
|
|
4510
|
-
}
|
|
4511
|
-
async function verifySubscription(emailOrCustomerId) {
|
|
4512
|
-
const current = loadSettings().premium;
|
|
4513
|
-
const identifier = emailOrCustomerId || current.customerId || current.email;
|
|
4514
|
-
if (!identifier) {
|
|
4515
|
-
return current;
|
|
4516
|
-
}
|
|
4517
|
-
try {
|
|
4518
|
-
const res = await fetch(`${VERIFICATION_API}/verify`, {
|
|
4519
|
-
method: "POST",
|
|
4520
|
-
headers: { "Content-Type": "application/json" },
|
|
4521
|
-
body: JSON.stringify({ identifier })
|
|
4522
|
-
});
|
|
4523
|
-
if (!res.ok) {
|
|
4524
|
-
console.warn("[Vessel Premium] Verification API returned", res.status);
|
|
4525
|
-
return current;
|
|
4526
|
-
}
|
|
4527
|
-
const data = await res.json();
|
|
4528
|
-
const updated = {
|
|
4529
|
-
status: data.status,
|
|
4530
|
-
customerId: data.customerId,
|
|
4531
|
-
email: data.email,
|
|
4532
|
-
validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4533
|
-
expiresAt: data.expiresAt
|
|
4534
|
-
};
|
|
4535
|
-
setSetting("premium", updated);
|
|
4536
|
-
return updated;
|
|
4537
|
-
} catch (err) {
|
|
4538
|
-
console.warn("[Vessel Premium] Verification failed:", err);
|
|
4539
|
-
return current;
|
|
4540
|
-
}
|
|
4541
|
-
}
|
|
4542
|
-
async function activateWithEmail(email) {
|
|
4543
|
-
if (!email.trim()) {
|
|
4544
|
-
return { ok: false, state: getPremiumState(), error: "Email is required" };
|
|
4545
|
-
}
|
|
4546
|
-
const state2 = await verifySubscription(email.trim());
|
|
4547
|
-
if (state2.status === "active" || state2.status === "trialing") {
|
|
4548
|
-
return { ok: true, state: state2 };
|
|
4549
|
-
}
|
|
4550
|
-
return {
|
|
4551
|
-
ok: false,
|
|
4552
|
-
state: state2,
|
|
4553
|
-
error: state2.status === "canceled" ? "Subscription is canceled. Resubscribe to continue." : state2.status === "past_due" ? "Subscription payment is past due. Update your payment method." : "No active subscription found for this email."
|
|
4554
|
-
};
|
|
4555
|
-
}
|
|
4556
|
-
let revalidationTimer = null;
|
|
4557
|
-
function startBackgroundRevalidation() {
|
|
4558
|
-
if (revalidationTimer) return;
|
|
4559
|
-
const { premium } = loadSettings();
|
|
4560
|
-
if (premium.customerId || premium.email) {
|
|
4561
|
-
const lastValidated = premium.validatedAt ? new Date(premium.validatedAt).getTime() : 0;
|
|
4562
|
-
if (Date.now() - lastValidated > REVALIDATION_INTERVAL_MS) {
|
|
4563
|
-
void verifySubscription();
|
|
4564
|
-
}
|
|
4565
|
-
}
|
|
4566
|
-
revalidationTimer = setInterval(() => {
|
|
4567
|
-
const { premium: p } = loadSettings();
|
|
4568
|
-
if (p.customerId || p.email) {
|
|
4569
|
-
void verifySubscription();
|
|
4570
|
-
}
|
|
4571
|
-
}, REVALIDATION_INTERVAL_MS);
|
|
4572
|
-
}
|
|
4573
|
-
function stopBackgroundRevalidation() {
|
|
4574
|
-
if (revalidationTimer) {
|
|
4575
|
-
clearInterval(revalidationTimer);
|
|
4576
|
-
revalidationTimer = null;
|
|
4577
|
-
}
|
|
4578
|
-
}
|
|
4579
|
-
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || "phc_OMeM3P5cxJwl14lOKxYad0Yre52xvjNfkLEFnPtXyM";
|
|
4580
|
-
const POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
|
|
4581
|
-
const BATCH_INTERVAL_MS = 6e4;
|
|
4582
|
-
const MAX_BATCH_SIZE = 50;
|
|
4583
|
-
function getDeviceIdPath() {
|
|
4584
|
-
return path.join(electron.app.getPath("userData"), ".vessel-device-id");
|
|
4585
|
-
}
|
|
4586
|
-
let deviceId = null;
|
|
4587
|
-
function getDeviceId() {
|
|
4588
|
-
if (deviceId) return deviceId;
|
|
4589
|
-
const idPath = getDeviceIdPath();
|
|
4590
|
-
try {
|
|
4591
|
-
deviceId = fs.readFileSync(idPath, "utf-8").trim();
|
|
4592
|
-
if (deviceId) return deviceId;
|
|
4593
|
-
} catch {
|
|
4594
|
-
}
|
|
4595
|
-
deviceId = crypto.randomUUID();
|
|
4596
|
-
try {
|
|
4597
|
-
fs.mkdirSync(path.dirname(idPath), { recursive: true });
|
|
4598
|
-
fs.writeFileSync(idPath, deviceId, "utf-8");
|
|
4599
|
-
} catch {
|
|
4738
|
+
if ("endpoint" in update) {
|
|
4739
|
+
state$1.mcp.endpoint = update.endpoint ?? null;
|
|
4740
|
+
}
|
|
4741
|
+
const prevStatus = state$1.mcp.status;
|
|
4742
|
+
state$1.mcp.status = update.status;
|
|
4743
|
+
state$1.mcp.message = update.message;
|
|
4744
|
+
if (prevStatus !== state$1.mcp.status) {
|
|
4745
|
+
for (const listener of mcpStatusChangeListeners) {
|
|
4746
|
+
listener(state$1.mcp.status);
|
|
4747
|
+
}
|
|
4600
4748
|
}
|
|
4601
|
-
return deviceId;
|
|
4602
4749
|
}
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4750
|
+
const VAULT_FILENAME = "vessel-vault.enc";
|
|
4751
|
+
const KEY_FILENAME = "vessel-vault.key";
|
|
4752
|
+
const ALGORITHM = "aes-256-gcm";
|
|
4753
|
+
const IV_LENGTH = 12;
|
|
4754
|
+
const AUTH_TAG_LENGTH = 16;
|
|
4755
|
+
let cachedEntries = null;
|
|
4756
|
+
function getVaultDir() {
|
|
4757
|
+
return electron.app.getPath("userData");
|
|
4609
4758
|
}
|
|
4610
|
-
function
|
|
4611
|
-
|
|
4612
|
-
eventQueue.push({
|
|
4613
|
-
event,
|
|
4614
|
-
properties: {
|
|
4615
|
-
...properties,
|
|
4616
|
-
premium_status: isPremium() ? "premium" : "free",
|
|
4617
|
-
app_version: electron.app.getVersion(),
|
|
4618
|
-
platform: process.platform,
|
|
4619
|
-
arch: process.arch
|
|
4620
|
-
},
|
|
4621
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4622
|
-
});
|
|
4623
|
-
if (eventQueue.length >= MAX_BATCH_SIZE) {
|
|
4624
|
-
void flush();
|
|
4625
|
-
}
|
|
4759
|
+
function getVaultPath() {
|
|
4760
|
+
return path$1.join(getVaultDir(), VAULT_FILENAME);
|
|
4626
4761
|
}
|
|
4627
|
-
function
|
|
4628
|
-
|
|
4629
|
-
tool_name: toolName,
|
|
4630
|
-
page_type: "unknown"
|
|
4631
|
-
});
|
|
4762
|
+
function getKeyPath() {
|
|
4763
|
+
return path$1.join(getVaultDir(), KEY_FILENAME);
|
|
4632
4764
|
}
|
|
4633
|
-
function
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4765
|
+
function getOrCreateEncryptionKey() {
|
|
4766
|
+
const keyPath = getKeyPath();
|
|
4767
|
+
if (fs$1.existsSync(keyPath)) {
|
|
4768
|
+
const encryptedKey = fs$1.readFileSync(keyPath);
|
|
4769
|
+
if (electron.safeStorage.isEncryptionAvailable()) {
|
|
4770
|
+
return electron.safeStorage.decryptString ? Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8") : encryptedKey;
|
|
4771
|
+
}
|
|
4772
|
+
return encryptedKey;
|
|
4773
|
+
}
|
|
4774
|
+
const key = crypto$1.randomBytes(32);
|
|
4775
|
+
fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
|
|
4776
|
+
if (electron.safeStorage.isEncryptionAvailable()) {
|
|
4777
|
+
const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
|
|
4778
|
+
fs$1.writeFileSync(keyPath, encrypted);
|
|
4779
|
+
} else {
|
|
4780
|
+
fs$1.writeFileSync(keyPath, key);
|
|
4781
|
+
fs$1.chmodSync(keyPath, 384);
|
|
4782
|
+
}
|
|
4783
|
+
return key;
|
|
4637
4784
|
}
|
|
4638
|
-
function
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
chrome_version: process.versions.chrome
|
|
4785
|
+
function encrypt(plaintext) {
|
|
4786
|
+
const key = getOrCreateEncryptionKey();
|
|
4787
|
+
const iv = crypto$1.randomBytes(IV_LENGTH);
|
|
4788
|
+
const cipher = crypto$1.createCipheriv(ALGORITHM, key, iv, {
|
|
4789
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
4644
4790
|
});
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4791
|
+
const encrypted = Buffer.concat([
|
|
4792
|
+
cipher.update(plaintext, "utf-8"),
|
|
4793
|
+
cipher.final()
|
|
4794
|
+
]);
|
|
4795
|
+
const authTag = cipher.getAuthTag();
|
|
4796
|
+
return Buffer.concat([iv, authTag, encrypted]);
|
|
4797
|
+
}
|
|
4798
|
+
function decrypt(data) {
|
|
4799
|
+
const key = getOrCreateEncryptionKey();
|
|
4800
|
+
const iv = data.subarray(0, IV_LENGTH);
|
|
4801
|
+
const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
|
|
4802
|
+
const ciphertext = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
|
|
4803
|
+
const decipher = crypto$1.createDecipheriv(ALGORITHM, key, iv, {
|
|
4804
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
4805
|
+
});
|
|
4806
|
+
decipher.setAuthTag(authTag);
|
|
4807
|
+
return decipher.update(ciphertext, void 0, "utf-8") + decipher.final("utf-8");
|
|
4648
4808
|
}
|
|
4649
|
-
function
|
|
4650
|
-
if (
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
duration_minutes: durationMinutes
|
|
4656
|
-
});
|
|
4657
|
-
sessionStartedAt = null;
|
|
4809
|
+
function loadVault() {
|
|
4810
|
+
if (cachedEntries) return cachedEntries;
|
|
4811
|
+
const vaultPath = getVaultPath();
|
|
4812
|
+
if (!fs$1.existsSync(vaultPath)) {
|
|
4813
|
+
cachedEntries = [];
|
|
4814
|
+
return cachedEntries;
|
|
4658
4815
|
}
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4816
|
+
try {
|
|
4817
|
+
const raw = fs$1.readFileSync(vaultPath);
|
|
4818
|
+
const json = decrypt(raw);
|
|
4819
|
+
cachedEntries = JSON.parse(json);
|
|
4820
|
+
return cachedEntries;
|
|
4821
|
+
} catch (err) {
|
|
4822
|
+
console.error("[Vessel Vault] Failed to load vault:", err);
|
|
4823
|
+
cachedEntries = [];
|
|
4824
|
+
return cachedEntries;
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
function saveVault(entries) {
|
|
4828
|
+
const json = JSON.stringify(entries, null, 2);
|
|
4829
|
+
const encrypted = encrypt(json);
|
|
4830
|
+
const vaultPath = getVaultPath();
|
|
4831
|
+
fs$1.mkdirSync(path$1.dirname(vaultPath), { recursive: true });
|
|
4832
|
+
fs$1.writeFileSync(vaultPath, encrypted);
|
|
4833
|
+
fs$1.chmodSync(vaultPath, 384);
|
|
4834
|
+
cachedEntries = entries;
|
|
4835
|
+
}
|
|
4836
|
+
function domainMatches(pattern, hostname) {
|
|
4837
|
+
const p = pattern.toLowerCase().trim();
|
|
4838
|
+
const h = hostname.toLowerCase().trim();
|
|
4839
|
+
if (p === h) return true;
|
|
4840
|
+
if (p.startsWith("*.")) {
|
|
4841
|
+
const suffix = p.slice(2);
|
|
4842
|
+
return h === suffix || h.endsWith("." + suffix);
|
|
4662
4843
|
}
|
|
4663
|
-
|
|
4844
|
+
return false;
|
|
4664
4845
|
}
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4846
|
+
function listEntries() {
|
|
4847
|
+
return loadVault().map(({ password, totpSecret, ...rest }) => rest);
|
|
4848
|
+
}
|
|
4849
|
+
function findEntriesForDomain(url) {
|
|
4850
|
+
let hostname;
|
|
4851
|
+
try {
|
|
4852
|
+
hostname = new URL(url).hostname;
|
|
4853
|
+
} catch {
|
|
4854
|
+
return [];
|
|
4670
4855
|
}
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
},
|
|
4681
|
-
timestamp: e.timestamp
|
|
4682
|
-
}))
|
|
4856
|
+
return loadVault().filter((e) => domainMatches(e.domainPattern, hostname)).map(({ password, totpSecret, ...rest }) => rest);
|
|
4857
|
+
}
|
|
4858
|
+
function addEntry(entry) {
|
|
4859
|
+
const entries = loadVault();
|
|
4860
|
+
const newEntry = {
|
|
4861
|
+
...entry,
|
|
4862
|
+
id: crypto$1.randomUUID(),
|
|
4863
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4864
|
+
useCount: 0
|
|
4683
4865
|
};
|
|
4866
|
+
entries.push(newEntry);
|
|
4867
|
+
saveVault(entries);
|
|
4868
|
+
return newEntry;
|
|
4869
|
+
}
|
|
4870
|
+
function updateEntry(id, updates) {
|
|
4871
|
+
const entries = loadVault();
|
|
4872
|
+
const idx = entries.findIndex((e) => e.id === id);
|
|
4873
|
+
if (idx === -1) return null;
|
|
4874
|
+
entries[idx] = { ...entries[idx], ...updates };
|
|
4875
|
+
saveVault(entries);
|
|
4876
|
+
return entries[idx];
|
|
4877
|
+
}
|
|
4878
|
+
function removeEntry(id) {
|
|
4879
|
+
const entries = loadVault();
|
|
4880
|
+
const idx = entries.findIndex((e) => e.id === id);
|
|
4881
|
+
if (idx === -1) return false;
|
|
4882
|
+
entries.splice(idx, 1);
|
|
4883
|
+
saveVault(entries);
|
|
4884
|
+
return true;
|
|
4885
|
+
}
|
|
4886
|
+
function recordUsage(id) {
|
|
4887
|
+
const entries = loadVault();
|
|
4888
|
+
const entry = entries.find((e) => e.id === id);
|
|
4889
|
+
if (!entry) return;
|
|
4890
|
+
entry.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4891
|
+
entry.useCount += 1;
|
|
4892
|
+
saveVault(entries);
|
|
4893
|
+
}
|
|
4894
|
+
function getCredential(id) {
|
|
4895
|
+
const entry = loadVault().find((e) => e.id === id);
|
|
4896
|
+
if (!entry) return null;
|
|
4897
|
+
return { username: entry.username, password: entry.password };
|
|
4898
|
+
}
|
|
4899
|
+
function getTotpSecret(id) {
|
|
4900
|
+
const entry = loadVault().find((e) => e.id === id);
|
|
4901
|
+
return entry?.totpSecret ?? null;
|
|
4902
|
+
}
|
|
4903
|
+
function generateTotpCode(secret) {
|
|
4904
|
+
const epoch = Math.floor(Date.now() / 1e3);
|
|
4905
|
+
const counter = Math.floor(epoch / 30);
|
|
4906
|
+
const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
4907
|
+
const cleanSecret = secret.replace(/[\s=-]/g, "").toUpperCase();
|
|
4908
|
+
let bits = "";
|
|
4909
|
+
for (const ch of cleanSecret) {
|
|
4910
|
+
const val = base32Chars.indexOf(ch);
|
|
4911
|
+
if (val === -1) continue;
|
|
4912
|
+
bits += val.toString(2).padStart(5, "0");
|
|
4913
|
+
}
|
|
4914
|
+
const keyBytes = Buffer.alloc(Math.floor(bits.length / 8));
|
|
4915
|
+
for (let i = 0; i < keyBytes.length; i++) {
|
|
4916
|
+
keyBytes[i] = parseInt(bits.slice(i * 8, i * 8 + 8), 2);
|
|
4917
|
+
}
|
|
4918
|
+
const counterBuf = Buffer.alloc(8);
|
|
4919
|
+
counterBuf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
|
|
4920
|
+
counterBuf.writeUInt32BE(counter & 4294967295, 4);
|
|
4921
|
+
const hmac = crypto$1.createHmac("sha1", keyBytes).update(counterBuf).digest();
|
|
4922
|
+
const offset = hmac[hmac.length - 1] & 15;
|
|
4923
|
+
const code = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
|
|
4924
|
+
return (code % 1e6).toString().padStart(6, "0");
|
|
4925
|
+
}
|
|
4926
|
+
const AUDIT_FILENAME = "vessel-vault-audit.jsonl";
|
|
4927
|
+
const MAX_ENTRIES = 1e3;
|
|
4928
|
+
function getAuditPath() {
|
|
4929
|
+
return path$1.join(electron.app.getPath("userData"), AUDIT_FILENAME);
|
|
4930
|
+
}
|
|
4931
|
+
function appendAuditEntry(entry) {
|
|
4684
4932
|
try {
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
}
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4933
|
+
const auditPath = getAuditPath();
|
|
4934
|
+
fs$1.mkdirSync(path$1.dirname(auditPath), { recursive: true });
|
|
4935
|
+
fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n");
|
|
4936
|
+
} catch (err) {
|
|
4937
|
+
console.error("[Vessel Vault] Failed to write audit log:", err);
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
function readAuditLog(limit = 100) {
|
|
4941
|
+
try {
|
|
4942
|
+
const auditPath = getAuditPath();
|
|
4943
|
+
if (!fs$1.existsSync(auditPath)) return [];
|
|
4944
|
+
const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
|
|
4945
|
+
return lines.slice(-Math.min(limit, MAX_ENTRIES)).map((line) => JSON.parse(line)).reverse();
|
|
4946
|
+
} catch (err) {
|
|
4947
|
+
console.error("[Vessel Vault] Failed to read audit log:", err);
|
|
4948
|
+
return [];
|
|
4694
4949
|
}
|
|
4695
4950
|
}
|
|
4696
4951
|
function isRichToolResult(value) {
|
|
@@ -4811,7 +5066,8 @@ class AnthropicProvider {
|
|
|
4811
5066
|
toolUseBlocks.push({
|
|
4812
5067
|
id: currentToolUse.id,
|
|
4813
5068
|
name: currentToolUse.name,
|
|
4814
|
-
input: {}
|
|
5069
|
+
input: {},
|
|
5070
|
+
_malformedArgs: currentToolUse.inputJson
|
|
4815
5071
|
});
|
|
4816
5072
|
}
|
|
4817
5073
|
currentToolUse = null;
|
|
@@ -4839,6 +5095,18 @@ class AnthropicProvider {
|
|
|
4839
5095
|
}
|
|
4840
5096
|
const toolResults = [];
|
|
4841
5097
|
for (const tb of toolUseBlocks) {
|
|
5098
|
+
if (tb._malformedArgs !== void 0) {
|
|
5099
|
+
onChunk(`
|
|
5100
|
+
<<tool:${tb.name}:⚠ invalid args>>
|
|
5101
|
+
`);
|
|
5102
|
+
toolResults.push({
|
|
5103
|
+
type: "tool_result",
|
|
5104
|
+
tool_use_id: tb.id,
|
|
5105
|
+
content: `Error: Invalid JSON in tool arguments — could not parse. Please retry with valid JSON. Raw input: ${tb._malformedArgs.slice(0, 200)}`,
|
|
5106
|
+
is_error: true
|
|
5107
|
+
});
|
|
5108
|
+
continue;
|
|
5109
|
+
}
|
|
4842
5110
|
const argSummary = tb.input.url || tb.input.text || tb.input.direction || "";
|
|
4843
5111
|
onChunk(`
|
|
4844
5112
|
<<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
|
|
@@ -5103,10 +5371,12 @@ class OpenAICompatProvider {
|
|
|
5103
5371
|
for (const tc of Object.values(toolCallAccums)) {
|
|
5104
5372
|
if (!tc.id) tc.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
5105
5373
|
}
|
|
5374
|
+
const malformedToolCalls = /* @__PURE__ */ new Set();
|
|
5106
5375
|
for (const tc of toolCalls) {
|
|
5107
5376
|
try {
|
|
5108
5377
|
JSON.parse(tc.argsJson || "{}");
|
|
5109
5378
|
} catch {
|
|
5379
|
+
malformedToolCalls.add(tc.id);
|
|
5110
5380
|
tc.argsJson = "{}";
|
|
5111
5381
|
}
|
|
5112
5382
|
}
|
|
@@ -5124,6 +5394,17 @@ class OpenAICompatProvider {
|
|
|
5124
5394
|
messages.push(assistantMsg);
|
|
5125
5395
|
if (toolCalls.length === 0) break;
|
|
5126
5396
|
for (const tc of toolCalls) {
|
|
5397
|
+
if (malformedToolCalls.has(tc.id)) {
|
|
5398
|
+
onChunk(`
|
|
5399
|
+
<<tool:${tc.name}:⚠ invalid args>>
|
|
5400
|
+
`);
|
|
5401
|
+
messages.push({
|
|
5402
|
+
role: "tool",
|
|
5403
|
+
tool_call_id: tc.id,
|
|
5404
|
+
content: `Error: Invalid JSON in tool arguments. The arguments could not be parsed. Please retry with valid JSON.`
|
|
5405
|
+
});
|
|
5406
|
+
continue;
|
|
5407
|
+
}
|
|
5127
5408
|
let args = {};
|
|
5128
5409
|
try {
|
|
5129
5410
|
args = JSON.parse(tc.argsJson || "{}");
|
|
@@ -5134,7 +5415,7 @@ class OpenAICompatProvider {
|
|
|
5134
5415
|
messages.push({
|
|
5135
5416
|
role: "tool",
|
|
5136
5417
|
tool_call_id: tc.id,
|
|
5137
|
-
content: `Error: Invalid JSON in tool arguments. Please retry with valid JSON
|
|
5418
|
+
content: `Error: Invalid JSON in tool arguments. Please retry with valid JSON.`
|
|
5138
5419
|
});
|
|
5139
5420
|
continue;
|
|
5140
5421
|
}
|
|
@@ -13183,6 +13464,41 @@ Exception: ${result.exceptionDetails}`);
|
|
|
13183
13464
|
}
|
|
13184
13465
|
);
|
|
13185
13466
|
}
|
|
13467
|
+
const sessionTrustedDomains = /* @__PURE__ */ new Set();
|
|
13468
|
+
async function requestConsent(request) {
|
|
13469
|
+
const domain = request.domain.toLowerCase();
|
|
13470
|
+
if (sessionTrustedDomains.has(domain)) {
|
|
13471
|
+
return { approved: true, trustForSession: true };
|
|
13472
|
+
}
|
|
13473
|
+
const focusedWindow = electron.BrowserWindow.getFocusedWindow();
|
|
13474
|
+
const { response } = await electron.dialog.showMessageBox(
|
|
13475
|
+
focusedWindow ?? (electron.BrowserWindow.getAllWindows()[0] || null),
|
|
13476
|
+
{
|
|
13477
|
+
type: "question",
|
|
13478
|
+
title: "Agent Credential Access",
|
|
13479
|
+
message: `Agent wants to sign in to ${request.domain}`,
|
|
13480
|
+
detail: [
|
|
13481
|
+
`Credential: ${request.credentialLabel}`,
|
|
13482
|
+
`Username: ${request.username}`,
|
|
13483
|
+
"",
|
|
13484
|
+
"The agent is requesting to fill a login form with stored credentials.",
|
|
13485
|
+
"Credential values will NOT be sent to the AI provider."
|
|
13486
|
+
].join("\n"),
|
|
13487
|
+
buttons: ["Deny", "Allow Once", "Allow for Session"],
|
|
13488
|
+
defaultId: 1,
|
|
13489
|
+
cancelId: 0,
|
|
13490
|
+
noLink: true
|
|
13491
|
+
}
|
|
13492
|
+
);
|
|
13493
|
+
if (response === 0) {
|
|
13494
|
+
return { approved: false, trustForSession: false };
|
|
13495
|
+
}
|
|
13496
|
+
const trustForSession = response === 2;
|
|
13497
|
+
if (trustForSession) {
|
|
13498
|
+
sessionTrustedDomains.add(domain);
|
|
13499
|
+
}
|
|
13500
|
+
return { approved: true, trustForSession };
|
|
13501
|
+
}
|
|
13186
13502
|
let httpServer = null;
|
|
13187
13503
|
let mcpAuthToken = null;
|
|
13188
13504
|
function asTextResponse(text) {
|
|
@@ -17332,6 +17648,244 @@ ${JSON.stringify(tableJson, null, 2)}`;
|
|
|
17332
17648
|
);
|
|
17333
17649
|
}
|
|
17334
17650
|
);
|
|
17651
|
+
server.registerTool(
|
|
17652
|
+
"vessel_vault_status",
|
|
17653
|
+
{
|
|
17654
|
+
title: "Check Vault Credentials",
|
|
17655
|
+
description: "Check whether stored credentials exist for a domain. Returns credential labels and usernames but NEVER password values. Use this before vault_login to verify credentials are available.",
|
|
17656
|
+
inputSchema: {
|
|
17657
|
+
domain: zod.z.string().describe(
|
|
17658
|
+
"The domain to check credentials for (e.g. 'github.com'). If omitted, checks the active tab's domain."
|
|
17659
|
+
).optional()
|
|
17660
|
+
}
|
|
17661
|
+
},
|
|
17662
|
+
async ({ domain }) => {
|
|
17663
|
+
let targetDomain = domain;
|
|
17664
|
+
if (!targetDomain) {
|
|
17665
|
+
const tab = tabManager.getActiveTab();
|
|
17666
|
+
if (!tab) return asTextResponse("Error: No active tab and no domain specified");
|
|
17667
|
+
try {
|
|
17668
|
+
targetDomain = new URL(tab.state.url).hostname;
|
|
17669
|
+
} catch {
|
|
17670
|
+
return asTextResponse("Error: Could not parse active tab URL");
|
|
17671
|
+
}
|
|
17672
|
+
}
|
|
17673
|
+
const matches = findEntriesForDomain(
|
|
17674
|
+
targetDomain.includes("://") ? targetDomain : `https://${targetDomain}`
|
|
17675
|
+
);
|
|
17676
|
+
if (matches.length === 0) {
|
|
17677
|
+
return asTextResponse(
|
|
17678
|
+
`No stored credentials found for ${targetDomain}. The user needs to add credentials in Settings > Agent Credential Vault before the agent can log in.`
|
|
17679
|
+
);
|
|
17680
|
+
}
|
|
17681
|
+
appendAuditEntry({
|
|
17682
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17683
|
+
credentialId: matches[0].id,
|
|
17684
|
+
credentialLabel: matches[0].label,
|
|
17685
|
+
domain: targetDomain,
|
|
17686
|
+
action: "status_check",
|
|
17687
|
+
approved: true
|
|
17688
|
+
});
|
|
17689
|
+
const summary = matches.map((m) => ` - "${m.label}" (${m.username})`).join("\n");
|
|
17690
|
+
return asTextResponse(
|
|
17691
|
+
`Found ${matches.length} credential(s) for ${targetDomain}:
|
|
17692
|
+
${summary}
|
|
17693
|
+
|
|
17694
|
+
Use vessel_vault_login to fill the login form. Credentials are filled directly — you will NOT see the password values.`
|
|
17695
|
+
);
|
|
17696
|
+
}
|
|
17697
|
+
);
|
|
17698
|
+
server.registerTool(
|
|
17699
|
+
"vessel_vault_login",
|
|
17700
|
+
{
|
|
17701
|
+
title: "Fill Login with Vault Credentials",
|
|
17702
|
+
description: "Fill a login form on the current page using stored credentials from the Agent Credential Vault. The credential values are filled directly into the page — they are NEVER returned in this response. The user will see a consent dialog before credentials are used.",
|
|
17703
|
+
inputSchema: {
|
|
17704
|
+
credential_label: zod.z.string().optional().describe(
|
|
17705
|
+
"Label of the credential to use. If omitted, uses the first matching credential for the current domain."
|
|
17706
|
+
),
|
|
17707
|
+
username_index: zod.z.number().optional().describe(
|
|
17708
|
+
"Element index of the username/email input field from read_page."
|
|
17709
|
+
),
|
|
17710
|
+
password_index: zod.z.number().optional().describe(
|
|
17711
|
+
"Element index of the password input field from read_page."
|
|
17712
|
+
),
|
|
17713
|
+
submit_after: zod.z.boolean().optional().describe(
|
|
17714
|
+
"Whether to click the submit button after filling credentials. Defaults to false."
|
|
17715
|
+
),
|
|
17716
|
+
submit_index: zod.z.number().optional().describe(
|
|
17717
|
+
"Element index of the submit button. Required if submit_after is true."
|
|
17718
|
+
)
|
|
17719
|
+
}
|
|
17720
|
+
},
|
|
17721
|
+
async ({
|
|
17722
|
+
credential_label,
|
|
17723
|
+
username_index,
|
|
17724
|
+
password_index,
|
|
17725
|
+
submit_after,
|
|
17726
|
+
submit_index
|
|
17727
|
+
}) => {
|
|
17728
|
+
const tab = tabManager.getActiveTab();
|
|
17729
|
+
if (!tab) return asTextResponse("Error: No active tab");
|
|
17730
|
+
const wc = tab.view.webContents;
|
|
17731
|
+
let hostname;
|
|
17732
|
+
try {
|
|
17733
|
+
hostname = new URL(tab.state.url).hostname;
|
|
17734
|
+
} catch {
|
|
17735
|
+
return asTextResponse("Error: Could not parse active tab URL");
|
|
17736
|
+
}
|
|
17737
|
+
const matches = findEntriesForDomain(`https://${hostname}`);
|
|
17738
|
+
if (matches.length === 0) {
|
|
17739
|
+
return asTextResponse(
|
|
17740
|
+
`No stored credentials for ${hostname}. The user needs to add credentials in Settings > Agent Credential Vault.`
|
|
17741
|
+
);
|
|
17742
|
+
}
|
|
17743
|
+
const match = credential_label ? matches.find(
|
|
17744
|
+
(m) => m.label.toLowerCase() === credential_label.toLowerCase()
|
|
17745
|
+
) : matches[0];
|
|
17746
|
+
if (!match) {
|
|
17747
|
+
return asTextResponse(
|
|
17748
|
+
`No credential named "${credential_label}" found for ${hostname}. Available: ${matches.map((m) => m.label).join(", ")}`
|
|
17749
|
+
);
|
|
17750
|
+
}
|
|
17751
|
+
const consent = await requestConsent({
|
|
17752
|
+
credentialLabel: match.label,
|
|
17753
|
+
username: match.username,
|
|
17754
|
+
domain: hostname
|
|
17755
|
+
});
|
|
17756
|
+
appendAuditEntry({
|
|
17757
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17758
|
+
credentialId: match.id,
|
|
17759
|
+
credentialLabel: match.label,
|
|
17760
|
+
domain: hostname,
|
|
17761
|
+
action: "login_fill",
|
|
17762
|
+
approved: consent.approved
|
|
17763
|
+
});
|
|
17764
|
+
if (!consent.approved) {
|
|
17765
|
+
return asTextResponse(
|
|
17766
|
+
`User denied credential access for ${hostname}. The agent should not retry without being asked.`
|
|
17767
|
+
);
|
|
17768
|
+
}
|
|
17769
|
+
const creds = getCredential(match.id);
|
|
17770
|
+
if (!creds) {
|
|
17771
|
+
return asTextResponse("Error: Credential not found in vault");
|
|
17772
|
+
}
|
|
17773
|
+
const results = [];
|
|
17774
|
+
if (username_index != null) {
|
|
17775
|
+
const usernameResult = await wc.executeJavaScript(
|
|
17776
|
+
`window.__vessel?.interactByIndex?.(${username_index}, "value", ${JSON.stringify(creds.username)}) || "Error: interactByIndex not available"`
|
|
17777
|
+
);
|
|
17778
|
+
results.push(`Username: ${usernameResult}`);
|
|
17779
|
+
}
|
|
17780
|
+
if (password_index != null) {
|
|
17781
|
+
const passwordResult = await wc.executeJavaScript(
|
|
17782
|
+
`window.__vessel?.interactByIndex?.(${password_index}, "value", ${JSON.stringify(creds.password)}) || "Error: interactByIndex not available"`
|
|
17783
|
+
);
|
|
17784
|
+
results.push(`Password: ${passwordResult.replace(/Typed into:.*/, "Typed into: [password field]")}`);
|
|
17785
|
+
}
|
|
17786
|
+
recordUsage(match.id);
|
|
17787
|
+
trackVaultAction("login_fill");
|
|
17788
|
+
if (submit_after && submit_index != null) {
|
|
17789
|
+
const submitResult = await wc.executeJavaScript(
|
|
17790
|
+
`window.__vessel?.interactByIndex?.(${submit_index}, "click") || "Error: interactByIndex not available"`
|
|
17791
|
+
);
|
|
17792
|
+
results.push(`Submit: ${submitResult}`);
|
|
17793
|
+
}
|
|
17794
|
+
return asTextResponse(
|
|
17795
|
+
[
|
|
17796
|
+
`Login form filled for ${hostname} using credential "${match.label}".`,
|
|
17797
|
+
...results,
|
|
17798
|
+
"",
|
|
17799
|
+
"Note: Credential values were filled directly into the page. They are NOT included in this response."
|
|
17800
|
+
].join("\n")
|
|
17801
|
+
);
|
|
17802
|
+
}
|
|
17803
|
+
);
|
|
17804
|
+
server.registerTool(
|
|
17805
|
+
"vessel_vault_totp",
|
|
17806
|
+
{
|
|
17807
|
+
title: "Fill TOTP Code from Vault",
|
|
17808
|
+
description: "Generate a TOTP 2FA code from a stored secret and fill it into a code input field. The TOTP secret and generated code are NEVER returned — only filled directly into the page.",
|
|
17809
|
+
inputSchema: {
|
|
17810
|
+
credential_label: zod.z.string().optional().describe(
|
|
17811
|
+
"Label of the credential whose TOTP secret to use. If omitted, uses the first matching credential with a TOTP secret."
|
|
17812
|
+
),
|
|
17813
|
+
code_index: zod.z.number().describe(
|
|
17814
|
+
"Element index of the TOTP/2FA code input field from read_page."
|
|
17815
|
+
),
|
|
17816
|
+
submit_after: zod.z.boolean().optional().describe("Whether to click submit after filling the code."),
|
|
17817
|
+
submit_index: zod.z.number().optional().describe("Element index of the submit button.")
|
|
17818
|
+
}
|
|
17819
|
+
},
|
|
17820
|
+
async ({ credential_label, code_index, submit_after, submit_index }) => {
|
|
17821
|
+
const tab = tabManager.getActiveTab();
|
|
17822
|
+
if (!tab) return asTextResponse("Error: No active tab");
|
|
17823
|
+
const wc = tab.view.webContents;
|
|
17824
|
+
let hostname;
|
|
17825
|
+
try {
|
|
17826
|
+
hostname = new URL(tab.state.url).hostname;
|
|
17827
|
+
} catch {
|
|
17828
|
+
return asTextResponse("Error: Could not parse active tab URL");
|
|
17829
|
+
}
|
|
17830
|
+
const matches = findEntriesForDomain(`https://${hostname}`);
|
|
17831
|
+
const match = credential_label ? matches.find(
|
|
17832
|
+
(m) => m.label.toLowerCase() === credential_label.toLowerCase()
|
|
17833
|
+
) : matches.find((m) => {
|
|
17834
|
+
const secret2 = getTotpSecret(m.id);
|
|
17835
|
+
return secret2 != null;
|
|
17836
|
+
});
|
|
17837
|
+
if (!match) {
|
|
17838
|
+
return asTextResponse(
|
|
17839
|
+
`No credential with TOTP secret found for ${hostname}.`
|
|
17840
|
+
);
|
|
17841
|
+
}
|
|
17842
|
+
const secret = getTotpSecret(match.id);
|
|
17843
|
+
if (!secret) {
|
|
17844
|
+
return asTextResponse(
|
|
17845
|
+
`Credential "${match.label}" does not have a TOTP secret configured.`
|
|
17846
|
+
);
|
|
17847
|
+
}
|
|
17848
|
+
const consent = await requestConsent({
|
|
17849
|
+
credentialLabel: match.label,
|
|
17850
|
+
username: match.username,
|
|
17851
|
+
domain: hostname
|
|
17852
|
+
});
|
|
17853
|
+
appendAuditEntry({
|
|
17854
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17855
|
+
credentialId: match.id,
|
|
17856
|
+
credentialLabel: match.label,
|
|
17857
|
+
domain: hostname,
|
|
17858
|
+
action: "totp_generate",
|
|
17859
|
+
approved: consent.approved
|
|
17860
|
+
});
|
|
17861
|
+
if (!consent.approved) {
|
|
17862
|
+
return asTextResponse(
|
|
17863
|
+
`User denied TOTP access for ${hostname}.`
|
|
17864
|
+
);
|
|
17865
|
+
}
|
|
17866
|
+
const code = generateTotpCode(secret);
|
|
17867
|
+
const fillResult = await wc.executeJavaScript(
|
|
17868
|
+
`window.__vessel?.interactByIndex?.(${code_index}, "value", ${JSON.stringify(code)}) || "Error: interactByIndex not available"`
|
|
17869
|
+
);
|
|
17870
|
+
recordUsage(match.id);
|
|
17871
|
+
trackVaultAction("totp_fill");
|
|
17872
|
+
const results = [`2FA code filled: ${fillResult.replace(/Typed into:.*/, "Typed into: [2FA field]")}`];
|
|
17873
|
+
if (submit_after && submit_index != null) {
|
|
17874
|
+
const submitResult = await wc.executeJavaScript(
|
|
17875
|
+
`window.__vessel?.interactByIndex?.(${submit_index}, "click") || "Error: interactByIndex not available"`
|
|
17876
|
+
);
|
|
17877
|
+
results.push(`Submit: ${submitResult}`);
|
|
17878
|
+
}
|
|
17879
|
+
return asTextResponse(
|
|
17880
|
+
[
|
|
17881
|
+
`TOTP code filled for ${hostname} using credential "${match.label}".`,
|
|
17882
|
+
...results,
|
|
17883
|
+
"",
|
|
17884
|
+
"Note: The TOTP code was filled directly into the page. It is NOT included in this response."
|
|
17885
|
+
].join("\n")
|
|
17886
|
+
);
|
|
17887
|
+
}
|
|
17888
|
+
);
|
|
17335
17889
|
server.registerTool(
|
|
17336
17890
|
"vessel_metrics",
|
|
17337
17891
|
{
|
|
@@ -17648,6 +18202,16 @@ function stopMcpServer() {
|
|
|
17648
18202
|
});
|
|
17649
18203
|
}
|
|
17650
18204
|
let activeChatProvider = null;
|
|
18205
|
+
function assertString(value, name) {
|
|
18206
|
+
if (typeof value !== "string") throw new Error(`${name} must be a string`);
|
|
18207
|
+
}
|
|
18208
|
+
function assertOptionalString(value, name) {
|
|
18209
|
+
if (value !== void 0 && typeof value !== "string") throw new Error(`${name} must be a string`);
|
|
18210
|
+
}
|
|
18211
|
+
function assertNumber(value, name) {
|
|
18212
|
+
if (typeof value !== "number" || Number.isNaN(value)) throw new Error(`${name} must be a number`);
|
|
18213
|
+
}
|
|
18214
|
+
const VALID_APPROVAL_MODES = /* @__PURE__ */ new Set(["auto", "confirm-dangerous", "manual"]);
|
|
17651
18215
|
function registerIpcHandlers(windowState, runtime2) {
|
|
17652
18216
|
const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
|
|
17653
18217
|
const sendToRendererViews = (channel, ...args) => {
|
|
@@ -17672,6 +18236,8 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17672
18236
|
layoutViews(windowState);
|
|
17673
18237
|
});
|
|
17674
18238
|
electron.ipcMain.handle(Channels.TAB_NAVIGATE, (_, id, url) => {
|
|
18239
|
+
assertString(id, "tabId");
|
|
18240
|
+
assertString(url, "url");
|
|
17675
18241
|
tabManager.navigateTab(id, url);
|
|
17676
18242
|
});
|
|
17677
18243
|
electron.ipcMain.handle(Channels.TAB_BACK, (_, id) => {
|
|
@@ -17723,6 +18289,9 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17723
18289
|
});
|
|
17724
18290
|
electron.ipcMain.handle(Channels.AI_FETCH_MODELS, async (_, config) => {
|
|
17725
18291
|
try {
|
|
18292
|
+
if (!config || typeof config !== "object" || !("id" in config)) {
|
|
18293
|
+
return { ok: false, models: [], error: "Invalid provider configuration" };
|
|
18294
|
+
}
|
|
17726
18295
|
const models = await fetchProviderModels(config);
|
|
17727
18296
|
return { ok: true, models };
|
|
17728
18297
|
} catch (err) {
|
|
@@ -17766,6 +18335,7 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17766
18335
|
windowState.sidebarView.setBounds({ x: 0, y: 0, width, height });
|
|
17767
18336
|
});
|
|
17768
18337
|
electron.ipcMain.handle(Channels.SIDEBAR_RESIZE, (_, width) => {
|
|
18338
|
+
assertNumber(width, "width");
|
|
17769
18339
|
const clamped = Math.max(240, Math.min(800, Math.round(width)));
|
|
17770
18340
|
windowState.uiState.sidebarWidth = clamped;
|
|
17771
18341
|
return clamped;
|
|
@@ -17789,11 +18359,13 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17789
18359
|
});
|
|
17790
18360
|
electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, () => getRuntimeHealth());
|
|
17791
18361
|
electron.ipcMain.handle(Channels.SETTINGS_SET, async (_, key, value) => {
|
|
18362
|
+
assertString(key, "key");
|
|
17792
18363
|
if (!SETTABLE_KEYS.has(key)) {
|
|
17793
18364
|
throw new Error(`Unknown setting key: ${key}`);
|
|
17794
18365
|
}
|
|
17795
18366
|
const settingsKey = key;
|
|
17796
18367
|
const updatedSettings = setSetting(settingsKey, value);
|
|
18368
|
+
trackSettingChanged(key);
|
|
17797
18369
|
if (key === "approvalMode") {
|
|
17798
18370
|
runtime2.setApprovalMode(value);
|
|
17799
18371
|
}
|
|
@@ -17810,6 +18382,11 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17810
18382
|
electron.ipcMain.handle(
|
|
17811
18383
|
Channels.AGENT_SET_APPROVAL_MODE,
|
|
17812
18384
|
(_, mode) => {
|
|
18385
|
+
assertString(mode, "mode");
|
|
18386
|
+
if (!VALID_APPROVAL_MODES.has(mode)) {
|
|
18387
|
+
throw new Error(`Invalid approval mode: ${mode}`);
|
|
18388
|
+
}
|
|
18389
|
+
trackApprovalModeChanged(mode);
|
|
17813
18390
|
setSetting("approvalMode", mode);
|
|
17814
18391
|
return runtime2.setApprovalMode(mode);
|
|
17815
18392
|
}
|
|
@@ -17840,17 +18417,23 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17840
18417
|
electron.ipcMain.handle(
|
|
17841
18418
|
Channels.FOLDER_CREATE,
|
|
17842
18419
|
(_, name, summary) => {
|
|
18420
|
+
trackBookmarkAction("folder_create");
|
|
17843
18421
|
return createFolderWithSummary(name, summary);
|
|
17844
18422
|
}
|
|
17845
18423
|
);
|
|
17846
18424
|
electron.ipcMain.handle(
|
|
17847
18425
|
Channels.BOOKMARK_SAVE,
|
|
17848
|
-
(_, url, title, folderId, note) =>
|
|
18426
|
+
(_, url, title, folderId, note) => {
|
|
18427
|
+
trackBookmarkAction("save");
|
|
18428
|
+
return saveBookmark(url, title, folderId, note);
|
|
18429
|
+
}
|
|
17849
18430
|
);
|
|
17850
18431
|
electron.ipcMain.handle(Channels.BOOKMARK_REMOVE, (_, id) => {
|
|
18432
|
+
trackBookmarkAction("remove");
|
|
17851
18433
|
return removeBookmark(id);
|
|
17852
18434
|
});
|
|
17853
18435
|
electron.ipcMain.handle(Channels.FOLDER_REMOVE, (_, id) => {
|
|
18436
|
+
trackBookmarkAction("folder_remove");
|
|
17854
18437
|
return removeFolder(id);
|
|
17855
18438
|
});
|
|
17856
18439
|
electron.ipcMain.handle(
|
|
@@ -18002,13 +18585,22 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18002
18585
|
return getPremiumState();
|
|
18003
18586
|
});
|
|
18004
18587
|
electron.ipcMain.handle(Channels.PREMIUM_ACTIVATE, async (_, email) => {
|
|
18588
|
+
assertString(email, "email");
|
|
18589
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
|
|
18590
|
+
return { ok: false, state: getPremiumState(), error: "Invalid email format" };
|
|
18591
|
+
}
|
|
18592
|
+
trackPremiumFunnel("activation_attempted");
|
|
18005
18593
|
const result = await activateWithEmail(email);
|
|
18006
18594
|
if (result.ok) {
|
|
18595
|
+
trackPremiumFunnel("activation_succeeded", { status: result.state.status });
|
|
18007
18596
|
sendToRendererViews(Channels.PREMIUM_UPDATE, result.state);
|
|
18597
|
+
} else {
|
|
18598
|
+
trackPremiumFunnel("activation_failed", { status: result.state.status });
|
|
18008
18599
|
}
|
|
18009
18600
|
return result;
|
|
18010
18601
|
});
|
|
18011
18602
|
electron.ipcMain.handle(Channels.PREMIUM_CHECKOUT, async (_, email) => {
|
|
18603
|
+
trackPremiumFunnel("checkout_clicked");
|
|
18012
18604
|
const result = await getCheckoutUrl(email);
|
|
18013
18605
|
if (result.ok && result.url) {
|
|
18014
18606
|
tabManager.createTab(result.url);
|
|
@@ -18016,17 +18608,56 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18016
18608
|
return result;
|
|
18017
18609
|
});
|
|
18018
18610
|
electron.ipcMain.handle(Channels.PREMIUM_RESET, () => {
|
|
18611
|
+
trackPremiumFunnel("reset");
|
|
18019
18612
|
const state2 = resetPremium();
|
|
18020
18613
|
sendToRendererViews(Channels.PREMIUM_UPDATE, state2);
|
|
18021
18614
|
return state2;
|
|
18022
18615
|
});
|
|
18023
18616
|
electron.ipcMain.handle(Channels.PREMIUM_PORTAL, async () => {
|
|
18617
|
+
trackPremiumFunnel("portal_opened");
|
|
18024
18618
|
const result = await getPortalUrl();
|
|
18025
18619
|
if (result.ok && result.url) {
|
|
18026
18620
|
tabManager.createTab(result.url);
|
|
18027
18621
|
}
|
|
18028
18622
|
return result;
|
|
18029
18623
|
});
|
|
18624
|
+
electron.ipcMain.handle(Channels.VAULT_LIST, () => {
|
|
18625
|
+
return listEntries();
|
|
18626
|
+
});
|
|
18627
|
+
electron.ipcMain.handle(
|
|
18628
|
+
Channels.VAULT_ADD,
|
|
18629
|
+
(_, entry) => {
|
|
18630
|
+
if (!entry || typeof entry !== "object") throw new Error("Invalid vault entry");
|
|
18631
|
+
assertString(entry.label, "label");
|
|
18632
|
+
assertString(entry.domainPattern, "domainPattern");
|
|
18633
|
+
assertString(entry.username, "username");
|
|
18634
|
+
assertString(entry.password, "password");
|
|
18635
|
+
if (!entry.label.trim() || !entry.domainPattern.trim() || !entry.username.trim() || !entry.password.trim()) {
|
|
18636
|
+
throw new Error("Label, domain, username, and password are required");
|
|
18637
|
+
}
|
|
18638
|
+
assertOptionalString(entry.totpSecret, "totpSecret");
|
|
18639
|
+
assertOptionalString(entry.notes, "notes");
|
|
18640
|
+
trackVaultAction("credential_added");
|
|
18641
|
+
const created = addEntry(entry);
|
|
18642
|
+
return { id: created.id, label: created.label, domainPattern: created.domainPattern, username: created.username };
|
|
18643
|
+
}
|
|
18644
|
+
);
|
|
18645
|
+
electron.ipcMain.handle(
|
|
18646
|
+
Channels.VAULT_UPDATE,
|
|
18647
|
+
(_, id, updates) => {
|
|
18648
|
+
assertString(id, "id");
|
|
18649
|
+
if (!updates || typeof updates !== "object") throw new Error("Invalid updates");
|
|
18650
|
+
return updateEntry(id, updates) !== null;
|
|
18651
|
+
}
|
|
18652
|
+
);
|
|
18653
|
+
electron.ipcMain.handle(Channels.VAULT_REMOVE, (_, id) => {
|
|
18654
|
+
assertString(id, "id");
|
|
18655
|
+
trackVaultAction("credential_removed");
|
|
18656
|
+
return removeEntry(id);
|
|
18657
|
+
});
|
|
18658
|
+
electron.ipcMain.handle(Channels.VAULT_AUDIT_LOG, (_, limit) => {
|
|
18659
|
+
return readAuditLog(limit);
|
|
18660
|
+
});
|
|
18030
18661
|
electron.ipcMain.handle(Channels.WINDOW_MINIMIZE, () => {
|
|
18031
18662
|
mainWindow.minimize();
|
|
18032
18663
|
});
|