@quanta-intellect/vessel-browser 0.1.45 → 0.1.46

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/out/main/index.js CHANGED
@@ -32,12 +32,14 @@ const defaults = {
32
32
  premium: {
33
33
  status: "free",
34
34
  customerId: "",
35
+ verificationToken: "",
35
36
  email: "",
36
37
  validatedAt: "",
37
38
  expiresAt: ""
38
39
  }
39
40
  };
40
41
  const SAVE_DEBOUNCE_MS$3 = 150;
42
+ const CHAT_PROVIDER_SECRET_FILENAME = "vessel-chat-provider-secret";
41
43
  const SETTABLE_KEYS = new Set(Object.keys(defaults));
42
44
  let settings = null;
43
45
  let settingsIssues = [];
@@ -52,6 +54,80 @@ function getUserDataPath() {
52
54
  function getSettingsPath() {
53
55
  return path.join(getUserDataPath(), "vessel-settings.json");
54
56
  }
57
+ function getChatProviderSecretPath() {
58
+ return path.join(getUserDataPath(), CHAT_PROVIDER_SECRET_FILENAME);
59
+ }
60
+ function canUseSafeStorage() {
61
+ try {
62
+ return electron.safeStorage.isEncryptionAvailable();
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+ function readStoredProviderSecret() {
68
+ try {
69
+ const raw = fs.readFileSync(getChatProviderSecretPath());
70
+ const decoded = canUseSafeStorage() && electron.safeStorage.decryptString ? electron.safeStorage.decryptString(raw) : raw.toString("utf-8");
71
+ const parsed = JSON.parse(decoded);
72
+ if (parsed && typeof parsed === "object" && typeof parsed.providerId === "string" && typeof parsed.apiKey === "string") {
73
+ return parsed;
74
+ }
75
+ } catch {
76
+ }
77
+ return null;
78
+ }
79
+ function writeStoredProviderSecret(secret) {
80
+ const filePath = getChatProviderSecretPath();
81
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
82
+ const payload = JSON.stringify(secret);
83
+ if (canUseSafeStorage()) {
84
+ const encrypted = electron.safeStorage.encryptString(payload);
85
+ fs.writeFileSync(filePath, encrypted, { mode: 384 });
86
+ return;
87
+ }
88
+ fs.writeFileSync(filePath, payload, { mode: 384 });
89
+ }
90
+ function clearStoredProviderSecret() {
91
+ try {
92
+ fs.unlinkSync(getChatProviderSecretPath());
93
+ } catch {
94
+ }
95
+ }
96
+ function mergeChatProviderSecret(provider) {
97
+ if (!provider) return null;
98
+ const stored = readStoredProviderSecret();
99
+ const legacyApiKey = provider.apiKey?.trim() || "";
100
+ const apiKey = stored?.providerId === provider.id ? stored.apiKey : legacyApiKey;
101
+ if (legacyApiKey && stored?.providerId !== provider.id) {
102
+ writeStoredProviderSecret({ providerId: provider.id, apiKey: legacyApiKey });
103
+ }
104
+ return {
105
+ ...provider,
106
+ apiKey,
107
+ hasApiKey: Boolean(apiKey)
108
+ };
109
+ }
110
+ function buildPersistedSettings(source) {
111
+ return {
112
+ ...source,
113
+ chatProvider: source.chatProvider ? {
114
+ ...source.chatProvider,
115
+ apiKey: "",
116
+ hasApiKey: source.chatProvider.hasApiKey || Boolean(source.chatProvider.apiKey)
117
+ } : null
118
+ };
119
+ }
120
+ function getRendererSettings() {
121
+ const current = loadSettings();
122
+ return {
123
+ ...current,
124
+ chatProvider: current.chatProvider ? {
125
+ ...current.chatProvider,
126
+ apiKey: "",
127
+ hasApiKey: Boolean(current.chatProvider.apiKey)
128
+ } : null
129
+ };
130
+ }
55
131
  function getSettingsLoadIssues() {
56
132
  return settingsIssues.map((issue) => ({ ...issue }));
57
133
  }
@@ -80,6 +156,7 @@ function loadSettings() {
80
156
  settings = {
81
157
  ...defaults,
82
158
  ...parsed,
159
+ chatProvider: mergeChatProviderSecret(parsed.chatProvider ?? null),
83
160
  mcpPort: sanitizePort(parsed.mcpPort ?? defaults.mcpPort),
84
161
  agentTranscriptMode: parsed.agentTranscriptMode === "off" || parsed.agentTranscriptMode === "summary" || parsed.agentTranscriptMode === "full" ? parsed.agentTranscriptMode : parsed.showAgentTranscript === false ? "off" : defaults.agentTranscriptMode
85
162
  };
@@ -104,7 +181,10 @@ function persistNow$3() {
104
181
  saveTimer$3 = null;
105
182
  }
106
183
  return fs.promises.mkdir(path.dirname(getSettingsPath()), { recursive: true }).then(
107
- () => fs.promises.writeFile(getSettingsPath(), JSON.stringify(settings, null, 2))
184
+ () => fs.promises.writeFile(
185
+ getSettingsPath(),
186
+ JSON.stringify(buildPersistedSettings(settings), null, 2)
187
+ )
108
188
  ).catch((err) => console.error("[Vessel] Failed to save settings:", err));
109
189
  }
110
190
  function saveSettings() {
@@ -121,6 +201,30 @@ function setSetting(key, value) {
121
201
  loadSettings();
122
202
  if (key === "mcpPort") {
123
203
  settings.mcpPort = sanitizePort(value);
204
+ } else if (key === "chatProvider") {
205
+ const nextProvider = value;
206
+ if (!nextProvider) {
207
+ clearStoredProviderSecret();
208
+ settings.chatProvider = null;
209
+ } else {
210
+ const existingSecret = readStoredProviderSecret();
211
+ const incomingApiKey = nextProvider.apiKey.trim();
212
+ const preserveExisting = !incomingApiKey && nextProvider.hasApiKey === true && existingSecret?.providerId === nextProvider.id;
213
+ const resolvedApiKey = preserveExisting ? existingSecret?.apiKey || "" : incomingApiKey;
214
+ if (resolvedApiKey) {
215
+ writeStoredProviderSecret({
216
+ providerId: nextProvider.id,
217
+ apiKey: resolvedApiKey
218
+ });
219
+ } else {
220
+ clearStoredProviderSecret();
221
+ }
222
+ settings.chatProvider = {
223
+ ...nextProvider,
224
+ apiKey: resolvedApiKey,
225
+ hasApiKey: Boolean(resolvedApiKey)
226
+ };
227
+ }
124
228
  } else {
125
229
  settings[key] = value;
126
230
  }
@@ -160,7 +264,31 @@ function checkDomainPolicy(url) {
160
264
  function matchesDomain(hostname, policyDomain) {
161
265
  return hostname === policyDomain || hostname.endsWith("." + policyDomain);
162
266
  }
267
+ const ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
268
+ function isSafeNavigationURL(url) {
269
+ try {
270
+ const parsed = new URL(url);
271
+ return ALLOWED_SCHEMES.has(parsed.protocol);
272
+ } catch {
273
+ return false;
274
+ }
275
+ }
276
+ function assertSafeURL(url) {
277
+ if (!isSafeNavigationURL(url)) {
278
+ throw new Error(
279
+ `Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`
280
+ );
281
+ }
282
+ }
283
+ function assertPermittedNavigationURL(url) {
284
+ assertSafeURL(url);
285
+ const policyError = checkDomainPolicy(url);
286
+ if (policyError) {
287
+ throw new Error(policyError);
288
+ }
289
+ }
163
290
  const MAX_CUSTOM_HISTORY = 50;
291
+ const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
164
292
  class Tab {
165
293
  id;
166
294
  view;
@@ -181,6 +309,32 @@ class Tab {
181
309
  urlForwardStack = [];
182
310
  lastCommittedUrl = "";
183
311
  navigatingViaHistory = false;
312
+ isReaderModeDataUrl(url) {
313
+ return this._state.isReaderMode && url.startsWith(READER_MODE_DATA_URL_PREFIX);
314
+ }
315
+ getNavigationBlockReason(url) {
316
+ if (!url) {
317
+ return "Blocked navigation to empty URL";
318
+ }
319
+ if (url.startsWith("about:") || this.isReaderModeDataUrl(url)) {
320
+ return null;
321
+ }
322
+ try {
323
+ assertSafeURL(url);
324
+ } catch (error) {
325
+ return error instanceof Error ? error.message : "Blocked unsafe navigation";
326
+ }
327
+ return checkDomainPolicy(url);
328
+ }
329
+ guardedLoadURL(url, options) {
330
+ const blockReason = this.getNavigationBlockReason(url);
331
+ if (blockReason) {
332
+ console.warn(`[Tab] ${blockReason}`);
333
+ return blockReason;
334
+ }
335
+ void this.view.webContents.loadURL(url, options);
336
+ return null;
337
+ }
184
338
  constructor(id, url, onChange, options) {
185
339
  this.id = id;
186
340
  this.parentWindow = options?.parentWindow;
@@ -198,10 +352,11 @@ class Tab {
198
352
  nodeIntegration: false
199
353
  }
200
354
  });
355
+ const initialUrl = url || "about:blank";
201
356
  this._state = {
202
357
  id,
203
358
  title: "New Tab",
204
- url: url || "about:blank",
359
+ url: initialUrl,
205
360
  favicon: "",
206
361
  isLoading: false,
207
362
  canGoBack: false,
@@ -222,13 +377,22 @@ class Tab {
222
377
  });
223
378
  this.setupListeners();
224
379
  if (url) {
225
- this.lastCommittedUrl = url;
226
- this.view.webContents.loadURL(url);
380
+ const error = this.guardedLoadURL(url);
381
+ if (error) {
382
+ this._state.url = "about:blank";
383
+ } else {
384
+ this.lastCommittedUrl = url;
385
+ }
227
386
  }
228
387
  }
229
388
  setupListeners() {
230
389
  const wc = this.view.webContents;
231
390
  wc.setWindowOpenHandler(({ url, disposition }) => {
391
+ const error = this.getNavigationBlockReason(url);
392
+ if (error) {
393
+ console.warn(`[Tab] ${error}`);
394
+ return { action: "deny" };
395
+ }
232
396
  this.onOpenUrl?.({
233
397
  url,
234
398
  background: disposition === "background-tab",
@@ -236,6 +400,18 @@ class Tab {
236
400
  });
237
401
  return { action: "deny" };
238
402
  });
403
+ const blockNavigation = (event, url, context) => {
404
+ const error = this.getNavigationBlockReason(url);
405
+ if (!error) return;
406
+ event.preventDefault();
407
+ console.warn(`[Tab] ${context}: ${error}`);
408
+ };
409
+ wc.on("will-navigate", (event, url) => {
410
+ blockNavigation(event, url, "Blocked top-level navigation");
411
+ });
412
+ wc.on("will-redirect", (event, url) => {
413
+ blockNavigation(event, url, "Blocked redirect");
414
+ });
239
415
  const syncNavigationState = () => {
240
416
  this._state.title = wc.getTitle() || this._state.title || "New Tab";
241
417
  this._state.url = wc.getURL() || this._state.url;
@@ -402,17 +578,12 @@ class Tab {
402
578
  url = `https://duckduckgo.com/?q=${encodeURIComponent(url)}`;
403
579
  }
404
580
  }
405
- if (!/^https?:\/\//i.test(url) && !url.startsWith("about:")) {
406
- return `Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`;
407
- }
408
- const policyError = checkDomainPolicy(url);
409
- if (policyError) return policyError;
410
581
  if (postBody) {
411
582
  const params = new URLSearchParams();
412
583
  for (const [key, value] of Object.entries(postBody)) {
413
584
  params.set(key, value);
414
585
  }
415
- this.view.webContents.loadURL(url, {
586
+ return this.guardedLoadURL(url, {
416
587
  method: "POST",
417
588
  extraHeaders: "Content-Type: application/x-www-form-urlencoded\r\n",
418
589
  postData: [
@@ -422,27 +593,39 @@ class Tab {
422
593
  }
423
594
  ]
424
595
  });
425
- } else {
426
- this.view.webContents.loadURL(url);
427
596
  }
428
- return null;
597
+ return this.guardedLoadURL(url);
429
598
  }
430
599
  goBack() {
431
600
  const previousUrl = this.urlHistory.pop();
432
601
  if (!previousUrl) return false;
602
+ const currentUrl = this.lastCommittedUrl;
433
603
  this.navigatingViaHistory = true;
434
- this.urlForwardStack.push(this.lastCommittedUrl);
604
+ this.urlForwardStack.push(currentUrl);
605
+ const error = this.guardedLoadURL(previousUrl);
606
+ if (error) {
607
+ this.navigatingViaHistory = false;
608
+ this.urlForwardStack.pop();
609
+ this.urlHistory.push(previousUrl);
610
+ return false;
611
+ }
435
612
  this.lastCommittedUrl = previousUrl;
436
- this.view.webContents.loadURL(previousUrl);
437
613
  return true;
438
614
  }
439
615
  goForward() {
440
616
  const nextUrl = this.urlForwardStack.pop();
441
617
  if (!nextUrl) return false;
618
+ const currentUrl = this.lastCommittedUrl;
442
619
  this.navigatingViaHistory = true;
443
- this.urlHistory.push(this.lastCommittedUrl);
620
+ this.urlHistory.push(currentUrl);
621
+ const error = this.guardedLoadURL(nextUrl);
622
+ if (error) {
623
+ this.navigatingViaHistory = false;
624
+ this.urlHistory.pop();
625
+ this.urlForwardStack.push(nextUrl);
626
+ return false;
627
+ }
444
628
  this.lastCommittedUrl = nextUrl;
445
- this.view.webContents.loadURL(nextUrl);
446
629
  return true;
447
630
  }
448
631
  canGoBack() {
@@ -2446,7 +2629,8 @@ const Channels = {
2446
2629
  DOWNLOAD_DONE: "download:done",
2447
2630
  // Premium
2448
2631
  PREMIUM_GET_STATE: "premium:get-state",
2449
- PREMIUM_ACTIVATE: "premium:activate",
2632
+ PREMIUM_ACTIVATION_START: "premium:activation-start",
2633
+ PREMIUM_ACTIVATION_VERIFY: "premium:activation-verify",
2450
2634
  PREMIUM_CHECKOUT: "premium:checkout",
2451
2635
  PREMIUM_PORTAL: "premium:portal",
2452
2636
  PREMIUM_RESET: "premium:reset",
@@ -3382,6 +3566,7 @@ function resetPremium() {
3382
3566
  const fresh = {
3383
3567
  status: "free",
3384
3568
  customerId: "",
3569
+ verificationToken: "",
3385
3570
  email: "",
3386
3571
  validatedAt: "",
3387
3572
  expiresAt: ""
@@ -3414,39 +3599,22 @@ async function getCheckoutUrl(email) {
3414
3599
  }
3415
3600
  }
3416
3601
  async function getPortalUrl() {
3417
- const { premium } = loadSettings();
3418
- if (!premium.customerId) {
3419
- return { ok: false, error: "No active subscription" };
3420
- }
3421
- try {
3422
- const res = await fetch(`${VERIFICATION_API}/portal`, {
3423
- method: "POST",
3424
- headers: { "Content-Type": "application/json" },
3425
- body: JSON.stringify({ customerId: premium.customerId })
3426
- });
3427
- if (!res.ok) {
3428
- return { ok: false, error: `HTTP ${res.status}` };
3429
- }
3430
- const { url } = await res.json();
3431
- return { ok: true, url };
3432
- } catch (err) {
3433
- return {
3434
- ok: false,
3435
- error: err instanceof Error ? err.message : "Failed to get portal URL"
3436
- };
3437
- }
3602
+ return {
3603
+ ok: false,
3604
+ error: "Billing portal access is temporarily disabled while we harden the premium API. Use the Stripe billing link from your subscription email for now."
3605
+ };
3438
3606
  }
3439
- async function verifySubscription(emailOrCustomerId) {
3607
+ async function verifySubscription(identifier) {
3440
3608
  const current = loadSettings().premium;
3441
- const identifier = emailOrCustomerId || current.customerId || current.email;
3442
- if (!identifier) {
3609
+ const verificationIdentifier = identifier || current.verificationToken || current.customerId;
3610
+ if (!verificationIdentifier) {
3443
3611
  return current;
3444
3612
  }
3445
3613
  try {
3446
3614
  const res = await fetch(`${VERIFICATION_API}/verify`, {
3447
3615
  method: "POST",
3448
3616
  headers: { "Content-Type": "application/json" },
3449
- body: JSON.stringify({ identifier })
3617
+ body: JSON.stringify({ identifier: verificationIdentifier })
3450
3618
  });
3451
3619
  if (!res.ok) {
3452
3620
  console.warn("[Vessel Premium] Verification API returned", res.status);
@@ -3455,8 +3623,9 @@ async function verifySubscription(emailOrCustomerId) {
3455
3623
  const data = await res.json();
3456
3624
  const updated = {
3457
3625
  status: data.status,
3458
- customerId: data.customerId,
3459
- email: data.email,
3626
+ customerId: data.customerId || current.customerId,
3627
+ verificationToken: data.verificationToken || verificationIdentifier,
3628
+ email: data.email || current.email,
3460
3629
  validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3461
3630
  expiresAt: data.expiresAt
3462
3631
  };
@@ -3467,34 +3636,104 @@ async function verifySubscription(emailOrCustomerId) {
3467
3636
  return current;
3468
3637
  }
3469
3638
  }
3470
- async function activateWithEmail(email) {
3471
- if (!email.trim()) {
3639
+ async function requestActivationCode(email) {
3640
+ const normalizedEmail = email.trim().toLowerCase();
3641
+ if (!normalizedEmail) {
3642
+ return { ok: false, error: "Email is required" };
3643
+ }
3644
+ try {
3645
+ const res = await fetch(`${VERIFICATION_API}/activate/start`, {
3646
+ method: "POST",
3647
+ headers: { "Content-Type": "application/json" },
3648
+ body: JSON.stringify({ email: normalizedEmail })
3649
+ });
3650
+ const data = await res.json().catch(() => ({}));
3651
+ if (!res.ok || !data.challengeToken) {
3652
+ return {
3653
+ ok: false,
3654
+ error: data.error || `HTTP ${res.status}`
3655
+ };
3656
+ }
3657
+ return {
3658
+ ok: true,
3659
+ email: normalizedEmail,
3660
+ challengeToken: data.challengeToken
3661
+ };
3662
+ } catch (err) {
3663
+ return {
3664
+ ok: false,
3665
+ error: err instanceof Error ? err.message : "Failed to send code"
3666
+ };
3667
+ }
3668
+ }
3669
+ async function verifyActivationCode(email, code, challengeToken) {
3670
+ const normalizedEmail = email.trim().toLowerCase();
3671
+ const trimmedCode = code.trim();
3672
+ if (!normalizedEmail) {
3472
3673
  return { ok: false, state: getPremiumState(), error: "Email is required" };
3473
3674
  }
3474
- const state2 = await verifySubscription(email.trim());
3475
- if (state2.status === "active" || state2.status === "trialing") {
3476
- return { ok: true, state: state2 };
3675
+ if (!trimmedCode) {
3676
+ return { ok: false, state: getPremiumState(), error: "Code is required" };
3677
+ }
3678
+ if (!challengeToken.trim()) {
3679
+ return {
3680
+ ok: false,
3681
+ state: getPremiumState(),
3682
+ error: "Request a new activation code and try again."
3683
+ };
3684
+ }
3685
+ try {
3686
+ const res = await fetch(`${VERIFICATION_API}/activate/verify`, {
3687
+ method: "POST",
3688
+ headers: { "Content-Type": "application/json" },
3689
+ body: JSON.stringify({
3690
+ email: normalizedEmail,
3691
+ code: trimmedCode,
3692
+ challengeToken: challengeToken.trim()
3693
+ })
3694
+ });
3695
+ const data = await res.json().catch(() => ({}));
3696
+ if (!res.ok) {
3697
+ return {
3698
+ ok: false,
3699
+ state: getPremiumState(),
3700
+ error: data.error || `HTTP ${res.status}`
3701
+ };
3702
+ }
3703
+ const updated = {
3704
+ status: data.status ?? "free",
3705
+ customerId: data.customerId || "",
3706
+ verificationToken: data.verificationToken || "",
3707
+ email: data.email || normalizedEmail,
3708
+ validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3709
+ expiresAt: data.expiresAt || ""
3710
+ };
3711
+ setSetting("premium", updated);
3712
+ return { ok: isPremiumActiveState(updated), state: updated };
3713
+ } catch (err) {
3714
+ return {
3715
+ ok: false,
3716
+ state: getPremiumState(),
3717
+ error: err instanceof Error ? err.message : "Failed to verify code"
3718
+ };
3477
3719
  }
3478
- return {
3479
- ok: false,
3480
- state: state2,
3481
- 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."
3482
- };
3483
3720
  }
3484
3721
  let revalidationTimer = null;
3485
3722
  function startBackgroundRevalidation() {
3486
3723
  if (revalidationTimer) return;
3487
3724
  const { premium } = loadSettings();
3488
- if (premium.customerId || premium.email) {
3725
+ const identifier = premium.verificationToken || premium.customerId;
3726
+ if (identifier) {
3489
3727
  const lastValidated = premium.validatedAt ? new Date(premium.validatedAt).getTime() : 0;
3490
3728
  if (Date.now() - lastValidated > REVALIDATION_INTERVAL_MS) {
3491
- void verifySubscription();
3729
+ void verifySubscription(identifier);
3492
3730
  }
3493
3731
  }
3494
3732
  revalidationTimer = setInterval(() => {
3495
3733
  const { premium: p } = loadSettings();
3496
- if (p.customerId || p.email) {
3497
- void verifySubscription();
3734
+ const currentIdentifier = p.verificationToken || p.customerId;
3735
+ if (currentIdentifier) {
3736
+ void verifySubscription(currentIdentifier);
3498
3737
  }
3499
3738
  }, REVALIDATION_INTERVAL_MS);
3500
3739
  }
@@ -4185,6 +4424,20 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
4185
4424
  return meta;
4186
4425
  }
4187
4426
 
4427
+ function shouldExposeFieldValue(el) {
4428
+ if (!(el instanceof HTMLInputElement)) return false;
4429
+ var elType = (el.type || "").toLowerCase();
4430
+ if (elType !== "number") return false;
4431
+ var signals = [
4432
+ el.name,
4433
+ el.id,
4434
+ el.getAttribute("placeholder"),
4435
+ el.getAttribute("aria-label"),
4436
+ labelFor(el),
4437
+ ].filter(Boolean).join(" ").toLowerCase();
4438
+ return /\b(qty|quantity|count|items?)\b/.test(signals);
4439
+ }
4440
+
4188
4441
  function serializeInteractive(el, kind) {
4189
4442
  var base = {
4190
4443
  type: kind,
@@ -4219,7 +4472,6 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
4219
4472
  return {
4220
4473
  ...base,
4221
4474
  label: labelFor(el)?.slice(0, 100),
4222
- value: text(el.value),
4223
4475
  options: Array.from(el.options || []).map(function(option) { return { label: text(option.textContent || option.value) || option.value, value: option.value }; }).filter(function(o) { return o.label || o.value; }).slice(0, 25),
4224
4476
  required: el.hasAttribute("required") || undefined,
4225
4477
  ...fieldMeta(el),
@@ -4231,7 +4483,6 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
4231
4483
  ...base,
4232
4484
  label: labelFor(el)?.slice(0, 100),
4233
4485
  placeholder: text(el.getAttribute("placeholder")),
4234
- value: text(el.value),
4235
4486
  required: el.hasAttribute("required") || undefined,
4236
4487
  ...fieldMeta(el),
4237
4488
  };
@@ -4243,7 +4494,7 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
4243
4494
  label: labelFor(el)?.slice(0, 100),
4244
4495
  inputType: text(el.getAttribute("type")),
4245
4496
  placeholder: text(el.getAttribute("placeholder")),
4246
- value: (elType === "password" || elType === "checkbox" || elType === "radio") ? undefined : text(el.value),
4497
+ value: shouldExposeFieldValue(el) ? text(el.value) : undefined,
4247
4498
  required: el.hasAttribute("required") || undefined,
4248
4499
  ...fieldMeta(el),
4249
4500
  };
@@ -4499,7 +4750,7 @@ const SAFE_EXTRACTION_SCRIPT = String.raw`
4499
4750
  label: labelFor(el)?.slice(0, 100),
4500
4751
  inputType: text(el.getAttribute && el.getAttribute("type")),
4501
4752
  placeholder: text(el.getAttribute && el.getAttribute("placeholder")),
4502
- value: tag === "select" ? text(el.value) : (elType === "password" || elType === "checkbox" || elType === "radio") ? undefined : text(el.value),
4753
+ value: shouldExposeFieldValue(el) ? text(el.value) : undefined,
4503
4754
  options: tag === "select"
4504
4755
  ? Array.from(el.options || []).map(function(option) { return { label: text(option.textContent || option.value) || option.value, value: option.value }; }).filter(function(o) { return o.label || o.value; }).slice(0, 25)
4505
4756
  : undefined,
@@ -4752,11 +5003,13 @@ function normalizePageContent(value) {
4752
5003
  function generateReaderHTML(page) {
4753
5004
  const escapedTitle = escapeHtml(page.title);
4754
5005
  const escapedByline = escapeHtml(page.byline);
5006
+ const renderedContent = renderReaderContent(page);
4755
5007
  return `<!DOCTYPE html>
4756
5008
  <html lang="en">
4757
5009
  <head>
4758
5010
  <meta charset="utf-8">
4759
5011
  <meta name="viewport" content="width=device-width, initial-scale=1">
5012
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'">
4760
5013
  <title>${escapedTitle}</title>
4761
5014
  <style>
4762
5015
  * { margin: 0; padding: 0; box-sizing: border-box; }
@@ -4827,13 +5080,20 @@ function generateReaderHTML(page) {
4827
5080
  <div class="reader-container">
4828
5081
  <h1>${escapedTitle}</h1>
4829
5082
  ${escapedByline ? `<div class="byline">${escapedByline}</div>` : ""}
4830
- <div class="reader-content">${page.htmlContent || escapeHtml(page.content)}</div>
5083
+ <div class="reader-content">${renderedContent}</div>
4831
5084
  </div>
4832
5085
  </body>
4833
5086
  </html>`;
4834
5087
  }
5088
+ function renderReaderContent(page) {
5089
+ const source = (page.content || page.excerpt || "").trim();
5090
+ if (!source) {
5091
+ return "<p>No readable content was available for this page.</p>";
5092
+ }
5093
+ return source.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean).map((block) => `<p>${escapeHtml(block).replace(/\n/g, "<br>")}</p>`).join("\n");
5094
+ }
4835
5095
  function escapeHtml(str) {
4836
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5096
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
4837
5097
  }
4838
5098
  const mcpStatusChangeListeners = /* @__PURE__ */ new Set();
4839
5099
  const runtimeHealthChangeListeners = /* @__PURE__ */ new Set();
@@ -4927,24 +5187,24 @@ function getVaultPath() {
4927
5187
  function getKeyPath() {
4928
5188
  return path$1.join(getVaultDir(), KEY_FILENAME);
4929
5189
  }
5190
+ function assertVaultSecretStorageAvailable() {
5191
+ if (!electron.safeStorage.isEncryptionAvailable()) {
5192
+ throw new Error(
5193
+ "Agent Credential Vault requires OS-backed secret storage. Enable Keychain, DPAPI, or libsecret support and restart Vessel."
5194
+ );
5195
+ }
5196
+ }
4930
5197
  function getOrCreateEncryptionKey() {
5198
+ assertVaultSecretStorageAvailable();
4931
5199
  const keyPath = getKeyPath();
4932
5200
  if (fs$1.existsSync(keyPath)) {
4933
5201
  const encryptedKey = fs$1.readFileSync(keyPath);
4934
- if (electron.safeStorage.isEncryptionAvailable()) {
4935
- return electron.safeStorage.decryptString ? Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8") : encryptedKey;
4936
- }
4937
- return encryptedKey;
5202
+ return Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8");
4938
5203
  }
4939
5204
  const key = crypto$2.randomBytes(32);
4940
5205
  fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
4941
- if (electron.safeStorage.isEncryptionAvailable()) {
4942
- const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
4943
- fs$1.writeFileSync(keyPath, encrypted);
4944
- } else {
4945
- fs$1.writeFileSync(keyPath, key);
4946
- fs$1.chmodSync(keyPath, 384);
4947
- }
5206
+ const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
5207
+ fs$1.writeFileSync(keyPath, encrypted, { mode: 384 });
4948
5208
  return key;
4949
5209
  }
4950
5210
  function encrypt(plaintext) {
@@ -4985,8 +5245,9 @@ function loadVault() {
4985
5245
  return cachedEntries;
4986
5246
  } catch (err) {
4987
5247
  console.error("[Vessel Vault] Failed to load vault:", err);
4988
- cachedEntries = [];
4989
- return cachedEntries;
5248
+ throw new Error(
5249
+ "Could not unlock the Agent Credential Vault. Check that OS secret storage is available and that the stored vault key can be decrypted."
5250
+ );
4990
5251
  }
4991
5252
  }
4992
5253
  function saveVault(entries) {
@@ -6728,6 +6989,10 @@ function limitItems(items, max = MAX_STRUCTURED_ITEMS) {
6728
6989
  if (items.length <= max) return items;
6729
6990
  return items.slice(0, max);
6730
6991
  }
6992
+ function shouldRenderFieldValue(el) {
6993
+ const value = typeof el.value === "string" && el.value.trim() ? el.value.trim() : "";
6994
+ return Boolean(value) && isQuantityLike(el);
6995
+ }
6731
6996
  function formatElementMeta(el) {
6732
6997
  const meta = [];
6733
6998
  if (el.context && el.context !== "content") {
@@ -6795,7 +7060,7 @@ function formatElementMeta(el) {
6795
7060
  if (el.description) {
6796
7061
  meta.push(`desc="${el.description.slice(0, 80)}"`);
6797
7062
  }
6798
- if (el.value !== void 0 && el.value !== null && el.value !== "") {
7063
+ if (shouldRenderFieldValue(el)) {
6799
7064
  meta.push(`value="${el.value.slice(0, 60)}"`);
6800
7065
  }
6801
7066
  if (el.selector) {
@@ -6806,7 +7071,7 @@ function formatElementMeta(el) {
6806
7071
  }
6807
7072
  function summarizeElementValue(el) {
6808
7073
  const value = typeof el.value === "string" && el.value.trim() ? el.value.trim() : "";
6809
- if (!value) return null;
7074
+ if (!value || !shouldRenderFieldValue(el)) return null;
6810
7075
  if (el.type === "select") {
6811
7076
  return { label: "selected", value: value.slice(0, 60) };
6812
7077
  }
@@ -10138,22 +10403,6 @@ function formatDeadLinkMessage(label, result) {
10138
10403
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
10139
10404
  return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
10140
10405
  }
10141
- const ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
10142
- function isSafeNavigationURL(url) {
10143
- try {
10144
- const parsed = new URL(url);
10145
- return ALLOWED_SCHEMES.has(parsed.protocol);
10146
- } catch {
10147
- return false;
10148
- }
10149
- }
10150
- function assertSafeURL(url) {
10151
- if (!isSafeNavigationURL(url)) {
10152
- throw new Error(
10153
- `Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`
10154
- );
10155
- }
10156
- }
10157
10406
  async function captureScreenshot(wc) {
10158
10407
  for (let attempt = 0; attempt < 3; attempt += 1) {
10159
10408
  await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
@@ -10878,6 +11127,10 @@ function formatCompactToolResult(name, result) {
10878
11127
  const DEFAULT_PAGE_SCRIPT_TIMEOUT_MS = 1500;
10879
11128
  const QUIET_NAVIGATION_WINDOW_MS = 1200;
10880
11129
  const PAGE_SCRIPT_TIMEOUT = /* @__PURE__ */ Symbol("page-script-timeout");
11130
+ async function loadPermittedUrl$1(wc, url) {
11131
+ assertPermittedNavigationURL(url);
11132
+ await wc.loadURL(url);
11133
+ }
10881
11134
  function pageBusyError(action) {
10882
11135
  return `Error: Page is still busy; ${action} timed out waiting for page scripts. Retry in a moment.`;
10883
11136
  }
@@ -11987,8 +12240,7 @@ Go back and select a different product.`;
11987
12240
  const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
11988
12241
  if (hrefMatch) {
11989
12242
  try {
11990
- assertSafeURL(hrefMatch[1]);
11991
- await wc.loadURL(hrefMatch[1]);
12243
+ await loadPermittedUrl$1(wc, hrefMatch[1]);
11992
12244
  await waitForLoad$1(wc, 8e3);
11993
12245
  const hrefUrl = wc.getURL();
11994
12246
  if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
@@ -12055,8 +12307,7 @@ Go back and select a different product.`;
12055
12307
  const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
12056
12308
  if (hrefMatch) {
12057
12309
  try {
12058
- assertSafeURL(hrefMatch[1]);
12059
- await wc.loadURL(hrefMatch[1]);
12310
+ await loadPermittedUrl$1(wc, hrefMatch[1]);
12060
12311
  await waitForLoad$1(wc, 8e3);
12061
12312
  const hrefUrl = wc.getURL();
12062
12313
  if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
@@ -12151,8 +12402,7 @@ ${postActivationOverlayHint}`;
12151
12402
  const validation = await validateLinkDestination(elInfo.href);
12152
12403
  if (validation.status !== "dead") {
12153
12404
  try {
12154
- assertSafeURL(elInfo.href);
12155
- await wc.loadURL(elInfo.href);
12405
+ await loadPermittedUrl$1(wc, elInfo.href);
12156
12406
  await waitForLoad$1(wc, 8e3);
12157
12407
  const hrefFallbackUrl = wc.getURL();
12158
12408
  if (hrefFallbackUrl !== beforeUrl) {
@@ -13704,8 +13954,7 @@ async function submitForm$1(wc, args) {
13704
13954
  if (formInfo.params) {
13705
13955
  url.search = formInfo.params;
13706
13956
  }
13707
- assertSafeURL(url.toString());
13708
- wc.loadURL(url.toString());
13957
+ await loadPermittedUrl$1(wc, url.toString());
13709
13958
  await waitForPotentialNavigation$1(wc, beforeUrl);
13710
13959
  const afterUrl = wc.getURL();
13711
13960
  return afterUrl !== beforeUrl ? `Submitted form via GET -> ${afterUrl}` : "Submitted form via GET";
@@ -14342,8 +14591,7 @@ async function searchPage(wc, args) {
14342
14591
  const shortcut = buildSearchShortcut(wc.getURL(), query);
14343
14592
  if (shortcut) {
14344
14593
  const beforeUrl2 = wc.getURL();
14345
- assertSafeURL(shortcut.url);
14346
- wc.loadURL(shortcut.url);
14594
+ await loadPermittedUrl$1(wc, shortcut.url);
14347
14595
  await waitForPotentialNavigation$1(wc, beforeUrl2, 4e3);
14348
14596
  const afterUrl2 = wc.getURL();
14349
14597
  const applied = shortcut.appliedFilters.length > 0 ? ` (${shortcut.appliedFilters.join(", ")})` : "";
@@ -16775,6 +17023,10 @@ function clearMcpAuthFile() {
16775
17023
  function asTextResponse(text) {
16776
17024
  return { content: [{ type: "text", text }] };
16777
17025
  }
17026
+ async function loadPermittedUrl(wc, url) {
17027
+ assertPermittedNavigationURL(url);
17028
+ await wc.loadURL(url);
17029
+ }
16778
17030
  function asPromptResponse(text) {
16779
17031
  return {
16780
17032
  messages: [
@@ -17094,7 +17346,7 @@ async function clickResolvedSelector(wc, selector) {
17094
17346
  const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
17095
17347
  if (hrefMatch) {
17096
17348
  try {
17097
- await wc.loadURL(hrefMatch[1]);
17349
+ await loadPermittedUrl(wc, hrefMatch[1]);
17098
17350
  await waitForLoad(wc, 8e3);
17099
17351
  const hrefUrl = wc.getURL();
17100
17352
  if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
@@ -17147,7 +17399,7 @@ ${overlayHint2}${actionsSuffix}`;
17147
17399
  const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
17148
17400
  if (hrefMatch) {
17149
17401
  try {
17150
- await wc.loadURL(hrefMatch[1]);
17402
+ await loadPermittedUrl(wc, hrefMatch[1]);
17151
17403
  await waitForLoad(wc, 8e3);
17152
17404
  const hrefUrl = wc.getURL();
17153
17405
  if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
@@ -18058,8 +18310,7 @@ async function submitForm(wc, index, selector) {
18058
18310
  if (formInfo.params) {
18059
18311
  url.search = formInfo.params;
18060
18312
  }
18061
- assertSafeURL(url.toString());
18062
- wc.loadURL(url.toString());
18313
+ await loadPermittedUrl(wc, url.toString());
18063
18314
  await waitForPotentialNavigation(wc, beforeUrl);
18064
18315
  const afterUrl = wc.getURL();
18065
18316
  return afterUrl !== beforeUrl ? `Submitted form via GET -> ${afterUrl}` : "Submitted form via GET";
@@ -22211,7 +22462,7 @@ function registerIpcHandlers(windowState, runtime2) {
22211
22462
  return windowState.uiState.settingsOpen;
22212
22463
  });
22213
22464
  electron.ipcMain.handle(Channels.SETTINGS_GET, () => {
22214
- return loadSettings();
22465
+ return getRendererSettings();
22215
22466
  });
22216
22467
  electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, () => getRuntimeHealth());
22217
22468
  electron.ipcMain.handle(Channels.SETTINGS_SET, async (_, key, value) => {
@@ -22229,8 +22480,9 @@ function registerIpcHandlers(windowState, runtime2) {
22229
22480
  await stopMcpServer();
22230
22481
  await startMcpServer(tabManager, runtime2, updatedSettings.mcpPort);
22231
22482
  }
22232
- sendToRendererViews(Channels.SETTINGS_UPDATE, updatedSettings);
22233
- return updatedSettings;
22483
+ const rendererSettings = getRendererSettings();
22484
+ sendToRendererViews(Channels.SETTINGS_UPDATE, rendererSettings);
22485
+ return rendererSettings;
22234
22486
  });
22235
22487
  electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, () => runtime2.getState());
22236
22488
  electron.ipcMain.handle(Channels.AGENT_PAUSE, () => runtime2.pause());
@@ -22445,21 +22697,44 @@ function registerIpcHandlers(windowState, runtime2) {
22445
22697
  electron.ipcMain.handle(Channels.PREMIUM_GET_STATE, () => {
22446
22698
  return getPremiumState();
22447
22699
  });
22448
- electron.ipcMain.handle(Channels.PREMIUM_ACTIVATE, async (_, email) => {
22700
+ electron.ipcMain.handle(Channels.PREMIUM_ACTIVATION_START, async (_, email) => {
22449
22701
  assertString(email, "email");
22450
22702
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
22451
- return { ok: false, state: getPremiumState(), error: "Invalid email format" };
22703
+ return { ok: false, error: "Invalid email format" };
22452
22704
  }
22453
22705
  trackPremiumFunnel("activation_attempted");
22454
- const result = await activateWithEmail(email);
22455
- if (result.ok) {
22456
- trackPremiumFunnel("activation_succeeded", { status: result.state.status });
22457
- sendToRendererViews(Channels.PREMIUM_UPDATE, result.state);
22458
- } else {
22459
- trackPremiumFunnel("activation_failed", { status: result.state.status });
22706
+ const result = await requestActivationCode(email);
22707
+ if (!result.ok) {
22708
+ trackPremiumFunnel("activation_failed");
22460
22709
  }
22461
22710
  return result;
22462
22711
  });
22712
+ electron.ipcMain.handle(
22713
+ Channels.PREMIUM_ACTIVATION_VERIFY,
22714
+ async (_, email, code, challengeToken) => {
22715
+ assertString(email, "email");
22716
+ assertString(code, "code");
22717
+ assertString(challengeToken, "challengeToken");
22718
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
22719
+ return {
22720
+ ok: false,
22721
+ state: getPremiumState(),
22722
+ error: "Invalid email format"
22723
+ };
22724
+ }
22725
+ trackPremiumFunnel("activation_attempted");
22726
+ const result = await verifyActivationCode(email, code, challengeToken);
22727
+ if (result.ok) {
22728
+ trackPremiumFunnel("activation_succeeded", {
22729
+ status: result.state.status
22730
+ });
22731
+ sendToRendererViews(Channels.PREMIUM_UPDATE, result.state);
22732
+ } else {
22733
+ trackPremiumFunnel("activation_failed", { status: result.state.status });
22734
+ }
22735
+ return result;
22736
+ }
22737
+ );
22463
22738
  electron.ipcMain.handle(Channels.PREMIUM_CHECKOUT, async (_, email) => {
22464
22739
  trackPremiumFunnel("checkout_clicked");
22465
22740
  const result = await getCheckoutUrl(email);