@quanta-intellect/vessel-browser 0.1.92 → 0.1.95

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
@@ -104,10 +104,23 @@ function canUseSafeStorage$1() {
104
104
  return false;
105
105
  }
106
106
  }
107
+ function writePrivateFile(filePath2, data) {
108
+ fs.writeFileSync(filePath2, data, { mode: 384 });
109
+ try {
110
+ fs.chmodSync(filePath2, 384);
111
+ } catch {
112
+ }
113
+ }
114
+ function assertSafeStorageAvailable() {
115
+ if (!canUseSafeStorage$1()) {
116
+ throw new Error("OS-backed secret storage is unavailable; refusing to store secrets on disk.");
117
+ }
118
+ }
107
119
  function readStoredProviderSecret() {
108
120
  try {
121
+ if (!canUseSafeStorage$1()) return null;
109
122
  const raw = fs.readFileSync(getChatProviderSecretPath());
110
- const decoded = canUseSafeStorage$1() && electron.safeStorage.decryptString ? electron.safeStorage.decryptString(raw) : raw.toString("utf-8");
123
+ const decoded = electron.safeStorage.decryptString(raw);
111
124
  const parsed = JSON.parse(decoded);
112
125
  if (parsed && typeof parsed === "object" && typeof parsed.providerId === "string" && typeof parsed.apiKey === "string") {
113
126
  return parsed;
@@ -117,15 +130,12 @@ function readStoredProviderSecret() {
117
130
  return null;
118
131
  }
119
132
  function writeStoredProviderSecret(secret) {
133
+ assertSafeStorageAvailable();
120
134
  const filePath2 = getChatProviderSecretPath();
121
135
  fs.mkdirSync(path.dirname(filePath2), { recursive: true });
122
136
  const payload = JSON.stringify(secret);
123
- if (canUseSafeStorage$1()) {
124
- const encrypted = electron.safeStorage.encryptString(payload);
125
- fs.writeFileSync(filePath2, encrypted, { mode: 384 });
126
- return;
127
- }
128
- fs.writeFileSync(filePath2, payload, { mode: 384 });
137
+ const encrypted = electron.safeStorage.encryptString(payload);
138
+ writePrivateFile(filePath2, encrypted);
129
139
  }
130
140
  function clearStoredProviderSecret() {
131
141
  try {
@@ -138,8 +148,9 @@ function getCodexTokensPath() {
138
148
  }
139
149
  function readStoredCodexTokens() {
140
150
  try {
151
+ if (!canUseSafeStorage$1()) return null;
141
152
  const raw = fs.readFileSync(getCodexTokensPath());
142
- const decoded = canUseSafeStorage$1() && electron.safeStorage.decryptString ? electron.safeStorage.decryptString(raw) : raw.toString("utf-8");
153
+ const decoded = electron.safeStorage.decryptString(raw);
143
154
  const parsed = JSON.parse(decoded);
144
155
  if (parsed && typeof parsed === "object" && typeof parsed.accessToken === "string" && typeof parsed.refreshToken === "string") {
145
156
  return parsed;
@@ -149,15 +160,12 @@ function readStoredCodexTokens() {
149
160
  return null;
150
161
  }
151
162
  function writeStoredCodexTokens(tokens) {
163
+ assertSafeStorageAvailable();
152
164
  const filePath2 = getCodexTokensPath();
153
165
  fs.mkdirSync(path.dirname(filePath2), { recursive: true });
154
166
  const payload = JSON.stringify(tokens);
155
- if (canUseSafeStorage$1()) {
156
- const encrypted = electron.safeStorage.encryptString(payload);
157
- fs.writeFileSync(filePath2, encrypted, { mode: 384 });
158
- return;
159
- }
160
- fs.writeFileSync(filePath2, payload, { mode: 384 });
167
+ const encrypted = electron.safeStorage.encryptString(payload);
168
+ writePrivateFile(filePath2, encrypted);
161
169
  }
162
170
  function clearStoredCodexTokens() {
163
171
  try {
@@ -268,9 +276,10 @@ function persistNow() {
268
276
  return fs.promises.mkdir(path.dirname(getSettingsPath()), { recursive: true }).then(
269
277
  () => fs.promises.writeFile(
270
278
  getSettingsPath(),
271
- JSON.stringify(buildPersistedSettings(settings), null, 2)
279
+ JSON.stringify(buildPersistedSettings(settings), null, 2),
280
+ { encoding: "utf-8", mode: 384 }
272
281
  )
273
- ).catch((err) => logger$n.error("Failed to save settings:", err));
282
+ ).then(() => fs.promises.chmod(getSettingsPath(), 384).catch(() => void 0)).catch((err) => logger$n.error("Failed to save settings:", err));
274
283
  }
275
284
  function saveSettings() {
276
285
  saveDirty = true;
@@ -375,6 +384,28 @@ function assertPermittedNavigationURL(url) {
375
384
  throw new Error(policyError);
376
385
  }
377
386
  }
387
+ function loadPermittedNavigationURL(wc, url) {
388
+ assertPermittedNavigationURL(url);
389
+ return wc.loadURL(url);
390
+ }
391
+ function loadInternalDataURL(wc, dataUrl) {
392
+ if (!dataUrl.startsWith("data:text/html;charset=utf-8,")) {
393
+ throw new Error("Blocked unexpected internal data URL");
394
+ }
395
+ return wc.loadURL(dataUrl);
396
+ }
397
+ function loadTrustedAppURL(wc, url) {
398
+ const parsed = new URL(url);
399
+ if (!["file:", "http:", "https:"].includes(parsed.protocol)) {
400
+ throw new Error(`Blocked unexpected app URL scheme: ${parsed.protocol}`);
401
+ }
402
+ const isHttp = parsed.protocol === "http:" || parsed.protocol === "https:";
403
+ const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
404
+ if (isHttp && !isLocalhost) {
405
+ throw new Error(`Blocked unexpected app URL host: ${parsed.hostname}`);
406
+ }
407
+ return wc.loadURL(parsed.toString());
408
+ }
378
409
  const MAX_CUSTOM_HISTORY = 50;
379
410
  const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
380
411
  const logger$m = createLogger("Tab");
@@ -1103,7 +1134,7 @@ function createDebouncedJsonPersistence({
1103
1134
  data,
1104
1135
  typeof data === "string" ? { encoding: "utf-8", mode: 384 } : { mode: 384 }
1105
1136
  )
1106
- ).catch((err) => logger$l.error(`Failed to save ${logLabel}:`, err));
1137
+ ).then(() => fs.promises.chmod(filePath2, 384).catch(() => void 0)).catch((err) => logger$l.error(`Failed to save ${logLabel}:`, err));
1107
1138
  };
1108
1139
  const schedule = () => {
1109
1140
  saveDirty2 = true;
@@ -1164,9 +1195,9 @@ function save$3() {
1164
1195
  }
1165
1196
  function emit$4() {
1166
1197
  if (!state$5) return;
1167
- const snapshot = { highlights: [...state$5.highlights] };
1198
+ const snapshot2 = { highlights: [...state$5.highlights] };
1168
1199
  for (const listener of listeners$2) {
1169
- listener(snapshot);
1200
+ listener(snapshot2);
1170
1201
  }
1171
1202
  }
1172
1203
  function normalizeUrl$1(rawUrl) {
@@ -1897,9 +1928,9 @@ function save$2() {
1897
1928
  }
1898
1929
  function emit$3() {
1899
1930
  if (!state$4) return;
1900
- const snapshot = { entries: [...state$4.entries] };
1931
+ const snapshot2 = { entries: [...state$4.entries] };
1901
1932
  for (const listener of listeners$1) {
1902
- listener(snapshot);
1933
+ listener(snapshot2);
1903
1934
  }
1904
1935
  }
1905
1936
  function getState$1() {
@@ -2093,6 +2124,8 @@ class DevToolsSession {
2093
2124
  this.tabId = tabId;
2094
2125
  this.wc = wc;
2095
2126
  }
2127
+ tabId;
2128
+ wc;
2096
2129
  attached = false;
2097
2130
  attachingPromise = null;
2098
2131
  consoleDomainEnabled = false;
@@ -3108,11 +3141,11 @@ class TabManager {
3108
3141
  note
3109
3142
  };
3110
3143
  }
3111
- restoreSession(snapshot) {
3112
- const tabs = snapshot.tabs.length > 0 ? snapshot.tabs : [{ id: "", url: "about:blank", title: "New Tab" }];
3144
+ restoreSession(snapshot2) {
3145
+ const tabs = snapshot2.tabs.length > 0 ? snapshot2.tabs : [{ id: "", url: "about:blank", title: "New Tab" }];
3113
3146
  const activeIndex = Math.max(
3114
3147
  0,
3115
- Math.min(snapshot.activeIndex, tabs.length - 1)
3148
+ Math.min(snapshot2.activeIndex, tabs.length - 1)
3116
3149
  );
3117
3150
  this.destroyAllTabs();
3118
3151
  const restoredGroups = /* @__PURE__ */ new Map();
@@ -3375,6 +3408,7 @@ const Channels = {
3375
3408
  SETTINGS_UPDATE: "settings:update",
3376
3409
  SETTINGS_HEALTH_GET: "settings:health:get",
3377
3410
  SETTINGS_HEALTH_UPDATE: "settings:health:update",
3411
+ MCP_REGENERATE_TOKEN: "mcp:regenerate-token",
3378
3412
  // Bookmarks
3379
3413
  BOOKMARKS_GET: "bookmarks:get",
3380
3414
  BOOKMARKS_UPDATE: "bookmarks:update",
@@ -3875,8 +3909,8 @@ function load$2() {
3875
3909
  const next = /* @__PURE__ */ new Map();
3876
3910
  if (!Array.isArray(raw)) return next;
3877
3911
  for (const entry of raw) {
3878
- const snapshot = normalizeStoredSnapshot(entry);
3879
- if (snapshot) next.set(snapshot.url, snapshot);
3912
+ const snapshot2 = normalizeStoredSnapshot(entry);
3913
+ if (snapshot2) next.set(snapshot2.url, snapshot2);
3880
3914
  }
3881
3915
  return next;
3882
3916
  }
@@ -3903,7 +3937,7 @@ function getSnapshot(normalizedUrl) {
3903
3937
  function saveSnapshot(rawUrl, title, textContent, headings) {
3904
3938
  const s = load$2();
3905
3939
  const key2 = normalizeUrl(rawUrl);
3906
- const snapshot = {
3940
+ const snapshot2 = {
3907
3941
  url: key2,
3908
3942
  title,
3909
3943
  textContent: textContent.slice(0, MAX_TEXT_LENGTH),
@@ -3911,9 +3945,9 @@ function saveSnapshot(rawUrl, title, textContent, headings) {
3911
3945
  capturedAt: (/* @__PURE__ */ new Date()).toISOString()
3912
3946
  };
3913
3947
  s.delete(key2);
3914
- s.set(key2, snapshot);
3948
+ s.set(key2, snapshot2);
3915
3949
  persistence$5.schedule();
3916
- return snapshot;
3950
+ return snapshot2;
3917
3951
  }
3918
3952
  function flushPersist$2() {
3919
3953
  return persistence$5.flush();
@@ -4726,6 +4760,22 @@ const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || "phc_OMeM3P5cxJwl14lOKxYa
4726
4760
  const POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
4727
4761
  const BATCH_INTERVAL_MS = 6e4;
4728
4762
  const MAX_BATCH_SIZE = 50;
4763
+ const SENSITIVE_PROPERTY_RE = /url|uri|query|prompt|content|text|token|secret|key|password|credential|email|domain/i;
4764
+ const SENSITIVE_STRING_VALUE_RE = /https?:\/\/|www\.|[^\s@]+@[^\s@]+\.[^\s@]+|bearer\s+\S+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.|(?:sk|pk|rk|gh[pousr]|xox[baprs])-[-_A-Za-z0-9]{12,}/i;
4765
+ const EMPTY_PROPERTY_ALLOWLIST = /* @__PURE__ */ new Set();
4766
+ const TELEMETRY_PROPERTY_ALLOWLIST = {
4767
+ app_launched: /* @__PURE__ */ new Set(["electron_version", "chrome_version"]),
4768
+ app_session_ended: /* @__PURE__ */ new Set(["duration_minutes"]),
4769
+ tool_called: /* @__PURE__ */ new Set(["tool_name", "page_type"]),
4770
+ provider_configured: /* @__PURE__ */ new Set(["provider_id"]),
4771
+ page_type_detected: /* @__PURE__ */ new Set(["page_type"]),
4772
+ setting_changed: /* @__PURE__ */ new Set(["setting_key"]),
4773
+ approval_mode_changed: /* @__PURE__ */ new Set(["mode"]),
4774
+ bookmark_action: /* @__PURE__ */ new Set(["action"]),
4775
+ vault_action: /* @__PURE__ */ new Set(["action"]),
4776
+ extraction_failed: /* @__PURE__ */ new Set(["reason"]),
4777
+ premium_funnel: /* @__PURE__ */ new Set(["step", "status", "reason"])
4778
+ };
4729
4779
  function getDeviceIdPath() {
4730
4780
  return path.join(electron.app.getPath("userData"), ".vessel-device-id");
4731
4781
  }
@@ -4741,7 +4791,8 @@ function getDeviceId() {
4741
4791
  deviceId = crypto$1.randomUUID();
4742
4792
  try {
4743
4793
  fs.mkdirSync(path.dirname(idPath), { recursive: true });
4744
- fs.writeFileSync(idPath, deviceId, "utf-8");
4794
+ fs.writeFileSync(idPath, deviceId, { encoding: "utf-8", mode: 384 });
4795
+ fs.chmodSync(idPath, 384);
4745
4796
  } catch {
4746
4797
  }
4747
4798
  return deviceId;
@@ -4754,12 +4805,27 @@ function isEnabled() {
4754
4805
  if (process.env.VESSEL_DEV === "1") return false;
4755
4806
  return loadSettings().telemetryEnabled !== false;
4756
4807
  }
4808
+ function sanitizeTelemetryProperties(properties, allowedKeys) {
4809
+ const safe = {};
4810
+ for (const [key2, value] of Object.entries(properties)) {
4811
+ if (allowedKeys && !allowedKeys.has(key2)) continue;
4812
+ if (!allowedKeys?.has(key2) && SENSITIVE_PROPERTY_RE.test(key2)) continue;
4813
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
4814
+ if (typeof value === "string" && SENSITIVE_STRING_VALUE_RE.test(value)) {
4815
+ continue;
4816
+ }
4817
+ safe[key2] = typeof value === "string" ? value.slice(0, 120) : value;
4818
+ }
4819
+ }
4820
+ return safe;
4821
+ }
4757
4822
  function trackEvent(event, properties = {}) {
4758
4823
  if (!isEnabled()) return;
4824
+ const allowedKeys = TELEMETRY_PROPERTY_ALLOWLIST[event] ?? EMPTY_PROPERTY_ALLOWLIST;
4759
4825
  eventQueue.push({
4760
4826
  event,
4761
4827
  properties: {
4762
- ...properties,
4828
+ ...sanitizeTelemetryProperties(properties, allowedKeys),
4763
4829
  premium_status: isPremium() ? "premium" : "free",
4764
4830
  app_version: electron.app.getVersion(),
4765
4831
  platform: process.platform,
@@ -4794,8 +4860,8 @@ function trackBookmarkAction(action) {
4794
4860
  function trackVaultAction(action) {
4795
4861
  trackEvent("vault_action", { action });
4796
4862
  }
4797
- function trackExtractionFailed(domain, reason) {
4798
- trackEvent("extraction_failed", { domain, reason });
4863
+ function trackExtractionFailed(_domain, reason) {
4864
+ trackEvent("extraction_failed", { reason });
4799
4865
  }
4800
4866
  function trackPremiumFunnel(step, context) {
4801
4867
  trackEvent("premium_funnel", { step, ...context });
@@ -6774,9 +6840,9 @@ function getMcpStatus() {
6774
6840
  return state$3.mcp.status;
6775
6841
  }
6776
6842
  function emitRuntimeHealthChange() {
6777
- const snapshot = getRuntimeHealth();
6843
+ const snapshot2 = getRuntimeHealth();
6778
6844
  for (const listener of runtimeHealthChangeListeners) {
6779
- listener(snapshot);
6845
+ listener(snapshot2);
6780
6846
  }
6781
6847
  }
6782
6848
  const state$3 = {
@@ -6942,7 +7008,7 @@ function isClickReadLoop(names) {
6942
7008
  return clickReadPairs >= 2;
6943
7009
  }
6944
7010
  const ANTHROPIC_MAX_TOKENS = 4096;
6945
- function isRecord(value) {
7011
+ function isRecord$1(value) {
6946
7012
  return value !== null && typeof value === "object" && !Array.isArray(value);
6947
7013
  }
6948
7014
  function anthropicModelLikelySupportsThinking(model) {
@@ -7066,7 +7132,7 @@ class AnthropicProvider {
7066
7132
  } else if (event.type === "content_block_stop" && currentToolUse) {
7067
7133
  try {
7068
7134
  const input = JSON.parse(currentToolUse.inputJson || "{}");
7069
- if (!isRecord(input)) {
7135
+ if (!isRecord$1(input)) {
7070
7136
  throw new Error("Tool input must be a JSON object");
7071
7137
  }
7072
7138
  toolUseBlocks.push({
@@ -8473,6 +8539,20 @@ class OpenAICompatProvider {
8473
8539
  this.abortController?.abort();
8474
8540
  }
8475
8541
  }
8542
+ async function openExternalAllowlisted(url, rule) {
8543
+ const parsed = new URL(url);
8544
+ const schemes = rule.schemes ?? ["https:"];
8545
+ if (!schemes.includes(parsed.protocol)) {
8546
+ throw new Error(`Blocked external URL scheme: ${parsed.protocol}`);
8547
+ }
8548
+ if (parsed.username || parsed.password) {
8549
+ throw new Error("Blocked external URL with embedded credentials");
8550
+ }
8551
+ if (rule.hosts && !rule.hosts.includes(parsed.hostname)) {
8552
+ throw new Error(`Blocked external URL host: ${parsed.hostname}`);
8553
+ }
8554
+ await electron.shell.openExternal(parsed.toString());
8555
+ }
8476
8556
  const logger$g = createLogger("CodexOAuth");
8477
8557
  const ISSUER = "https://auth.openai.com";
8478
8558
  const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
@@ -8538,54 +8618,6 @@ function parseTokenExpiry(accessToken) {
8538
8618
  }
8539
8619
  return Date.now() + 36e5;
8540
8620
  }
8541
- async function exchangeIdTokenForApiKey(idToken) {
8542
- const body = new URLSearchParams({
8543
- grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
8544
- client_id: CLIENT_ID,
8545
- requested_token: "openai-api-key",
8546
- subject_token: idToken,
8547
- subject_token_type: "urn:ietf:params:oauth:token-type:id_token"
8548
- });
8549
- const response = await fetch(`${ISSUER}/oauth/token`, {
8550
- method: "POST",
8551
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
8552
- body: body.toString()
8553
- });
8554
- if (!response.ok) {
8555
- let errorMsg = `OpenAI API token exchange failed: ${response.status}`;
8556
- try {
8557
- const err = await response.json();
8558
- if (typeof err.error_description === "string") {
8559
- errorMsg = err.error_description;
8560
- } else if (typeof err.error === "string") {
8561
- errorMsg = err.error;
8562
- }
8563
- } catch {
8564
- }
8565
- throw new Error(errorMsg);
8566
- }
8567
- const data = await response.json();
8568
- if (!data.access_token) {
8569
- throw new Error("OpenAI API token exchange did not return an access token");
8570
- }
8571
- return data.access_token;
8572
- }
8573
- async function ensureCodexApiKey(tokens) {
8574
- if (tokens.apiKey) return tokens;
8575
- if (!tokens.idToken) return tokens;
8576
- try {
8577
- return {
8578
- ...tokens,
8579
- apiKey: await exchangeIdTokenForApiKey(tokens.idToken)
8580
- };
8581
- } catch (err) {
8582
- logger$g.warn(
8583
- "Codex API-key token exchange failed; continuing with ChatGPT OAuth tokens:",
8584
- err
8585
- );
8586
- return tokens;
8587
- }
8588
- }
8589
8621
  async function exchangeCodeForTokens(code, redirectUri, codeVerifier) {
8590
8622
  const body = new URLSearchParams({
8591
8623
  grant_type: "authorization_code",
@@ -8623,7 +8655,7 @@ async function exchangeCodeForTokens(code, redirectUri, codeVerifier) {
8623
8655
  accountId: claims?.accountId || "",
8624
8656
  accountEmail: claims?.email
8625
8657
  };
8626
- return ensureCodexApiKey(tokens);
8658
+ return tokens;
8627
8659
  }
8628
8660
  async function refreshAccessToken(tokens) {
8629
8661
  const body = new URLSearchParams({
@@ -8659,7 +8691,7 @@ async function refreshAccessToken(tokens) {
8659
8691
  accountId: claims?.accountId || tokens.accountId || "",
8660
8692
  accountEmail: claims?.email || tokens.accountEmail
8661
8693
  };
8662
- return ensureCodexApiKey(refreshedTokens);
8694
+ return refreshedTokens;
8663
8695
  }
8664
8696
  function startServer(port, pkce, expectedState, resolve, reject) {
8665
8697
  const server = http.createServer(async (req, res) => {
@@ -8809,7 +8841,7 @@ async function startCodexOAuth(onStatus) {
8809
8841
  activeFlow.port = port;
8810
8842
  const authUrl = buildAuthorizeUrl(port, pkce, state2);
8811
8843
  safeOnStatus("waiting");
8812
- electron.shell.openExternal(authUrl).catch((err) => {
8844
+ openExternalAllowlisted(authUrl, { hosts: ["auth.openai.com"] }).catch((err) => {
8813
8845
  logger$g.warn("Failed to open browser, user will need the URL:", err);
8814
8846
  });
8815
8847
  }).catch(wrappedReject);
@@ -8830,12 +8862,57 @@ const logger$f = createLogger("CodexProvider");
8830
8862
  const REFRESH_WINDOW_MS = 5 * 60 * 1e3;
8831
8863
  const CODEX_BACKEND_BASE_URL = "https://chatgpt.com/backend-api/codex";
8832
8864
  const CODEX_CLIENT_VERSION = "0.129.0";
8865
+ function isRecord(value) {
8866
+ return value !== null && typeof value === "object" && !Array.isArray(value);
8867
+ }
8868
+ async function createCodexFunctionCallOutput(functionCall, availableToolNames, onChunk, onToolCall) {
8869
+ const callId = functionCall.call_id || functionCall.id || "";
8870
+ const name = functionCall.name || "";
8871
+ if (!callId) {
8872
+ return {
8873
+ type: "function_call_output",
8874
+ call_id: callId,
8875
+ output: "Error: Function call was missing a call_id. Please retry the tool call."
8876
+ };
8877
+ }
8878
+ if (!name || !availableToolNames.has(name)) {
8879
+ onChunk(`
8880
+ <<tool:${name || "unknown"}:⚠ unsupported>>
8881
+ `);
8882
+ return {
8883
+ type: "function_call_output",
8884
+ call_id: callId,
8885
+ output: `Error: Unsupported tool${name ? `: ${name}` : ""}. Use one of the provided tools.`
8886
+ };
8887
+ }
8888
+ let args;
8889
+ try {
8890
+ const parsed = JSON.parse(functionCall.arguments || "{}");
8891
+ if (!isRecord(parsed)) throw new Error("Tool arguments must be a JSON object");
8892
+ args = parsed;
8893
+ } catch {
8894
+ onChunk(`
8895
+ <<tool:${name}:⚠ invalid args>>
8896
+ `);
8897
+ return {
8898
+ type: "function_call_output",
8899
+ call_id: callId,
8900
+ output: "Error: Invalid JSON in tool arguments. Please retry with a valid JSON object."
8901
+ };
8902
+ }
8903
+ const output = await onToolCall(name, args);
8904
+ return {
8905
+ type: "function_call_output",
8906
+ call_id: callId,
8907
+ output
8908
+ };
8909
+ }
8833
8910
  class CodexProvider {
8834
8911
  agentToolProfile;
8835
8912
  tokens;
8836
8913
  model;
8837
8914
  abortController = null;
8838
- constructor(tokens, model, _baseUrl) {
8915
+ constructor(tokens, model) {
8839
8916
  this.tokens = tokens;
8840
8917
  this.model = model;
8841
8918
  this.agentToolProfile = "default";
@@ -8854,7 +8931,7 @@ class CodexProvider {
8854
8931
  );
8855
8932
  }
8856
8933
  }
8857
- backendHeaders() {
8934
+ backendHeaders(turnState) {
8858
8935
  const headers = {
8859
8936
  Authorization: `Bearer ${this.tokens.accessToken}`,
8860
8937
  "Content-Type": "application/json",
@@ -8865,6 +8942,9 @@ class CodexProvider {
8865
8942
  if (this.tokens.accountId) {
8866
8943
  headers["ChatGPT-Account-ID"] = this.tokens.accountId;
8867
8944
  }
8945
+ if (turnState) {
8946
+ headers["x-codex-turn-state"] = turnState;
8947
+ }
8868
8948
  return headers;
8869
8949
  }
8870
8950
  buildInput(userMessage, history) {
@@ -8873,7 +8953,7 @@ class CodexProvider {
8873
8953
  input.push({
8874
8954
  type: "message",
8875
8955
  role: msg.role,
8876
- content: [{ type: "input_text", text: msg.content }]
8956
+ content: [{ type: msg.role === "assistant" ? "output_text" : "input_text", text: msg.content }]
8877
8957
  });
8878
8958
  }
8879
8959
  input.push({
@@ -8883,7 +8963,7 @@ class CodexProvider {
8883
8963
  });
8884
8964
  return input;
8885
8965
  }
8886
- handleStreamEvent(raw, onChunk, emittedTextFromDelta) {
8966
+ handleStreamEvent(raw, onChunk, acc) {
8887
8967
  if (!raw.trim() || raw.trim() === "[DONE]") return;
8888
8968
  let event;
8889
8969
  try {
@@ -8892,13 +8972,28 @@ class CodexProvider {
8892
8972
  return;
8893
8973
  }
8894
8974
  if (event.type === "response.output_text.delta" && event.delta) {
8895
- emittedTextFromDelta.value = true;
8975
+ acc.emittedTextFromDelta = true;
8976
+ acc.text += event.delta;
8896
8977
  onChunk(event.delta);
8897
8978
  return;
8898
8979
  }
8899
- if (event.type === "response.output_item.done" && !emittedTextFromDelta.value) {
8900
- const text = event.item?.content?.filter((item) => item.type === "output_text" && item.text).map((item) => item.text).join("");
8901
- if (text) onChunk(text);
8980
+ if (event.type === "response.function_call_arguments.delta" && event.delta) {
8981
+ const key2 = event.call_id || event.item_id || "";
8982
+ if (key2) {
8983
+ acc.functionCallArgs.set(key2, (acc.functionCallArgs.get(key2) || "") + event.delta);
8984
+ }
8985
+ return;
8986
+ }
8987
+ if (event.type === "response.output_item.done" && event.item) {
8988
+ const item = event.item;
8989
+ if (item.type === "function_call") {
8990
+ const key2 = item.call_id || item.id || "";
8991
+ const args = acc.functionCallArgs.get(key2) || item.arguments || "";
8992
+ acc.functionCallArgs.delete(key2);
8993
+ acc.items.push({ ...item, arguments: args });
8994
+ } else if (item.type === "message") {
8995
+ acc.items.push(item);
8996
+ }
8902
8997
  return;
8903
8998
  }
8904
8999
  if (event.type === "response.failed") {
@@ -8907,18 +9002,12 @@ class CodexProvider {
8907
9002
  throw new Error(message);
8908
9003
  }
8909
9004
  }
8910
- async streamCodexResponse(systemPrompt, userMessage, onChunk, history) {
9005
+ async streamCodexResponse(requestBody, onChunk, turnState) {
8911
9006
  const response = await fetch(`${CODEX_BACKEND_BASE_URL}/responses`, {
8912
9007
  method: "POST",
8913
- headers: this.backendHeaders(),
9008
+ headers: this.backendHeaders(turnState),
8914
9009
  signal: this.abortController?.signal,
8915
- body: JSON.stringify({
8916
- model: this.model,
8917
- instructions: systemPrompt,
8918
- input: this.buildInput(userMessage, history),
8919
- stream: true,
8920
- store: false
8921
- })
9010
+ body: JSON.stringify(requestBody)
8922
9011
  });
8923
9012
  if (!response.ok) {
8924
9013
  const text = await response.text().catch(() => "");
@@ -8929,33 +9018,53 @@ class CodexProvider {
8929
9018
  if (!response.body) {
8930
9019
  throw new Error("Codex backend returned an empty response stream");
8931
9020
  }
9021
+ const newTurnState = response.headers.get("x-codex-turn-state") || null;
8932
9022
  const reader = response.body.getReader();
8933
- const decoder = new TextDecoder();
8934
- let buffer = "";
8935
- const emittedTextFromDelta = { value: false };
8936
- while (true) {
8937
- const { value, done } = await reader.read();
8938
- if (done) break;
8939
- buffer += decoder.decode(value, { stream: true });
8940
- let separatorIndex;
8941
- while ((separatorIndex = buffer.indexOf("\n\n")) !== -1) {
8942
- const block = buffer.slice(0, separatorIndex);
8943
- buffer = buffer.slice(separatorIndex + 2);
8944
- const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
8945
- this.handleStreamEvent(data, onChunk, emittedTextFromDelta);
8946
- }
8947
- }
8948
- const trailing = buffer.trim();
8949
- if (trailing) {
8950
- const data = trailing.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
8951
- this.handleStreamEvent(data, onChunk, emittedTextFromDelta);
9023
+ try {
9024
+ const decoder = new TextDecoder();
9025
+ let buffer = "";
9026
+ const acc = {
9027
+ text: "",
9028
+ items: [],
9029
+ emittedTextFromDelta: false,
9030
+ functionCallArgs: /* @__PURE__ */ new Map()
9031
+ };
9032
+ while (true) {
9033
+ const { value, done } = await reader.read();
9034
+ if (done) break;
9035
+ buffer += decoder.decode(value, { stream: true });
9036
+ let separatorIndex;
9037
+ while ((separatorIndex = buffer.indexOf("\n\n")) !== -1) {
9038
+ const block = buffer.slice(0, separatorIndex);
9039
+ buffer = buffer.slice(separatorIndex + 2);
9040
+ const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
9041
+ this.handleStreamEvent(data, onChunk, acc);
9042
+ }
9043
+ }
9044
+ const trailing = buffer.trim();
9045
+ if (trailing) {
9046
+ const data = trailing.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
9047
+ this.handleStreamEvent(data, onChunk, acc);
9048
+ }
9049
+ return { text: acc.text, items: acc.items, turnState: newTurnState };
9050
+ } finally {
9051
+ reader.releaseLock();
8952
9052
  }
8953
9053
  }
8954
9054
  async streamQuery(systemPrompt, userMessage, onChunk, onEnd, history) {
8955
9055
  await this.ensureFreshTokens();
8956
9056
  this.abortController = new AbortController();
8957
9057
  try {
8958
- await this.streamCodexResponse(systemPrompt, userMessage, onChunk, history);
9058
+ await this.streamCodexResponse(
9059
+ {
9060
+ model: this.model,
9061
+ instructions: systemPrompt,
9062
+ input: this.buildInput(userMessage, history),
9063
+ stream: true,
9064
+ store: false
9065
+ },
9066
+ onChunk
9067
+ );
8959
9068
  } catch (err) {
8960
9069
  if (err.name !== "AbortError") {
8961
9070
  const msg = err instanceof Error ? err.message : String(err);
@@ -8969,11 +9078,59 @@ class CodexProvider {
8969
9078
  onEnd();
8970
9079
  }
8971
9080
  }
8972
- async streamAgentQuery(systemPrompt, userMessage, _tools, onChunk, _onToolCall, onEnd, history) {
9081
+ async streamAgentQuery(systemPrompt, userMessage, tools, onChunk, onToolCall, onEnd, history) {
8973
9082
  await this.ensureFreshTokens();
8974
9083
  this.abortController = new AbortController();
9084
+ const maxIterations = getEffectiveMaxIterations();
9085
+ const availableToolNames = new Set(tools.map((tool) => tool.name));
9086
+ let iterationsUsed = 0;
9087
+ const convertedTools = tools.map((tool) => ({
9088
+ type: "function",
9089
+ name: tool.name,
9090
+ description: tool.description || "",
9091
+ parameters: tool.input_schema
9092
+ }));
9093
+ let currentInput = this.buildInput(userMessage, history);
9094
+ let turnState = null;
8975
9095
  try {
8976
- await this.streamCodexResponse(systemPrompt, userMessage, onChunk, history);
9096
+ for (let i = 0; i < maxIterations; i++) {
9097
+ iterationsUsed = i + 1;
9098
+ const result = await this.streamCodexResponse(
9099
+ {
9100
+ model: this.model,
9101
+ instructions: systemPrompt,
9102
+ input: currentInput,
9103
+ tools: convertedTools,
9104
+ stream: true,
9105
+ store: false
9106
+ },
9107
+ onChunk,
9108
+ turnState || void 0
9109
+ );
9110
+ turnState = result.turnState || turnState;
9111
+ const functionCalls = result.items.filter(
9112
+ (item) => item.type === "function_call"
9113
+ );
9114
+ if (functionCalls.length === 0) {
9115
+ break;
9116
+ }
9117
+ currentInput = [];
9118
+ for (const fc of functionCalls) {
9119
+ currentInput.push(
9120
+ await createCodexFunctionCallOutput(
9121
+ fc,
9122
+ availableToolNames,
9123
+ onChunk,
9124
+ onToolCall
9125
+ )
9126
+ );
9127
+ }
9128
+ }
9129
+ if (iterationsUsed >= maxIterations) {
9130
+ onChunk(`
9131
+
9132
+ [Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
9133
+ }
8977
9134
  } catch (err) {
8978
9135
  if (err.name !== "AbortError") {
8979
9136
  const msg = err instanceof Error ? err.message : String(err);
@@ -9057,11 +9214,11 @@ function buildLlamaCppCtxWarning(ctxSize) {
9057
9214
  }
9058
9215
  async function fetchCodexBackendModels(tokens) {
9059
9216
  const url = new URL("https://chatgpt.com/backend-api/codex/models");
9060
- url.searchParams.set("client_version", "0.129.0");
9217
+ url.searchParams.set("client_version", CODEX_CLIENT_VERSION);
9061
9218
  const headers = {
9062
9219
  Authorization: `Bearer ${tokens.accessToken}`,
9063
9220
  originator: "codex_cli_rs",
9064
- "User-Agent": "codex_cli_rs/0.129.0 Vessel"
9221
+ "User-Agent": `codex_cli_rs/${CODEX_CLIENT_VERSION} Vessel`
9065
9222
  };
9066
9223
  if (tokens.accountId) {
9067
9224
  headers["ChatGPT-Account-ID"] = tokens.accountId;
@@ -9164,7 +9321,7 @@ function createProvider(config) {
9164
9321
  "OpenAI Codex requires authentication. Open settings to connect your ChatGPT account."
9165
9322
  );
9166
9323
  }
9167
- return new CodexProvider(tokens, normalized.model, normalized.baseUrl);
9324
+ return new CodexProvider(tokens, normalized.model);
9168
9325
  }
9169
9326
  return new OpenAICompatProvider(normalized);
9170
9327
  }
@@ -12476,9 +12633,9 @@ function assignDefinedBookmarkFields(bookmark, fields) {
12476
12633
  }
12477
12634
  function emit$2() {
12478
12635
  if (!state$2) return;
12479
- const snapshot = cloneState(state$2);
12636
+ const snapshot2 = cloneState(state$2);
12480
12637
  for (const listener of listeners) {
12481
- listener(snapshot);
12638
+ listener(snapshot2);
12482
12639
  }
12483
12640
  }
12484
12641
  function escapeBookmarkHtml(value) {
@@ -12999,7 +13156,7 @@ async function captureLiveHighlightSnapshot(wc, savedHighlights = []) {
12999
13156
  savedHighlights.map((highlight) => normalizeText(highlight.text)).filter(Boolean)
13000
13157
  );
13001
13158
  try {
13002
- const snapshot = await wc.executeJavaScript(`(() => {
13159
+ const snapshot2 = await wc.executeJavaScript(`(() => {
13003
13160
  const selection = window.getSelection?.()?.toString().trim() || "";
13004
13161
  const pageHighlights = Array.from(
13005
13162
  document.querySelectorAll("mark.__vessel-highlight-text[data-vessel-highlight]")
@@ -13021,7 +13178,7 @@ async function captureLiveHighlightSnapshot(wc, savedHighlights = []) {
13021
13178
  };
13022
13179
  })()`, true);
13023
13180
  const seen = /* @__PURE__ */ new Set();
13024
- const pageHighlights = (snapshot.pageHighlights ?? []).map((highlight) => ({
13181
+ const pageHighlights = (snapshot2.pageHighlights ?? []).map((highlight) => ({
13025
13182
  text: normalizeText(highlight.text),
13026
13183
  color: highlight.color?.trim() || void 0
13027
13184
  })).filter((highlight) => highlight.text.length > 0).filter((highlight) => {
@@ -13033,24 +13190,24 @@ async function captureLiveHighlightSnapshot(wc, savedHighlights = []) {
13033
13190
  ...highlight,
13034
13191
  persisted: savedTexts.has(highlight.text)
13035
13192
  }));
13036
- const activeSelection = normalizeText(snapshot.activeSelection) || void 0;
13193
+ const activeSelection = normalizeText(snapshot2.activeSelection) || void 0;
13037
13194
  return { activeSelection, pageHighlights };
13038
13195
  } catch {
13039
13196
  return { pageHighlights: [] };
13040
13197
  }
13041
13198
  }
13042
- function formatLiveSelectionSection(snapshot) {
13199
+ function formatLiveSelectionSection(snapshot2) {
13043
13200
  const sections = [];
13044
- if (snapshot.activeSelection) {
13045
- const preview = snapshot.activeSelection.length > 400 ? `${snapshot.activeSelection.slice(0, 397)}...` : snapshot.activeSelection;
13201
+ if (snapshot2.activeSelection) {
13202
+ const preview = snapshot2.activeSelection.length > 400 ? `${snapshot2.activeSelection.slice(0, 397)}...` : snapshot2.activeSelection;
13046
13203
  sections.push(
13047
13204
  `## Active User Selection
13048
13205
  The user currently has this text selected on screen:
13049
13206
  - "${preview}"`
13050
13207
  );
13051
13208
  }
13052
- if (snapshot.pageHighlights.length > 0) {
13053
- const lines = snapshot.pageHighlights.map((highlight) => {
13209
+ if (snapshot2.pageHighlights.length > 0) {
13210
+ const lines = snapshot2.pageHighlights.map((highlight) => {
13054
13211
  const preview = highlight.text.length > 180 ? `${highlight.text.slice(0, 177)}...` : highlight.text;
13055
13212
  const details = [
13056
13213
  highlight.persisted ? "saved" : "visible only",
@@ -13551,7 +13708,7 @@ async function saveNamedSession(tabManager, name) {
13551
13708
  const entries = await captureLocalStorageForOrigin(tabManager, origin);
13552
13709
  localStorage.push({ origin, entries });
13553
13710
  }
13554
- const snapshot = tabManager.snapshotSession(`Named session: ${normalizedName}`);
13711
+ const snapshot2 = tabManager.snapshotSession(`Named session: ${normalizedName}`);
13555
13712
  const domains = [...new Set(cookies.map((cookie) => cookie.domain))].sort();
13556
13713
  const now = (/* @__PURE__ */ new Date()).toISOString();
13557
13714
  const data = {
@@ -13563,7 +13720,7 @@ async function saveNamedSession(tabManager, name) {
13563
13720
  domains,
13564
13721
  cookies,
13565
13722
  localStorage,
13566
- snapshot
13723
+ snapshot: snapshot2
13567
13724
  };
13568
13725
  writeSessionFile(getSessionPath(normalizedName), data);
13569
13726
  return {
@@ -13918,8 +14075,8 @@ function compactSearchLikeResult(text) {
13918
14075
  return limitText(cleaned, 16, 1400);
13919
14076
  }
13920
14077
  const summary = cleaned.slice(0, markerIndex).trim();
13921
- const snapshot = cleaned.slice(markerIndex + marker.length).trim();
13922
- return [summary, compactReadPageResult(snapshot)].filter(Boolean).join("\n\n");
14078
+ const snapshot2 = cleaned.slice(markerIndex + marker.length).trim();
14079
+ return [summary, compactReadPageResult(snapshot2)].filter(Boolean).join("\n\n");
13923
14080
  }
13924
14081
  function compactCurrentTabResult(text) {
13925
14082
  try {
@@ -14968,7 +15125,7 @@ async function inspectElement(wc, selector, limit = 8) {
14968
15125
  return lines.join("\n");
14969
15126
  }
14970
15127
  async function getLocaleSnapshot(wc) {
14971
- const snapshot = await executePageScript(
15128
+ const snapshot2 = await executePageScript(
14972
15129
  wc,
14973
15130
  `
14974
15131
  (function() {
@@ -14987,13 +15144,13 @@ async function getLocaleSnapshot(wc) {
14987
15144
  label: "locale snapshot"
14988
15145
  }
14989
15146
  );
14990
- if (!snapshot || snapshot === PAGE_SCRIPT_TIMEOUT || typeof snapshot !== "object") {
15147
+ if (!snapshot2 || snapshot2 === PAGE_SCRIPT_TIMEOUT || typeof snapshot2 !== "object") {
14991
15148
  return null;
14992
15149
  }
14993
15150
  return {
14994
- lang: typeof snapshot.lang === "string" ? snapshot.lang.trim() : "",
14995
- url: typeof snapshot.url === "string" ? snapshot.url : wc.getURL(),
14996
- title: typeof snapshot.title === "string" ? snapshot.title : wc.getTitle()
15151
+ lang: typeof snapshot2.lang === "string" ? snapshot2.lang.trim() : "",
15152
+ url: typeof snapshot2.url === "string" ? snapshot2.url : wc.getURL(),
15153
+ title: typeof snapshot2.title === "string" ? snapshot2.title : wc.getTitle()
14997
15154
  };
14998
15155
  }
14999
15156
  function primaryLanguageTag(value) {
@@ -15009,31 +15166,31 @@ function localeChanged(before, after) {
15009
15166
  const localeHint = /[?&](lang|locale|language|hl)=|\/(ja|jp|en|fr|de|es|it|ko|zh)(\/|$)/i;
15010
15167
  return before.url !== after.url && localeHint.test(after.url);
15011
15168
  }
15012
- async function restoreLocaleSnapshot(wc, snapshot) {
15013
- if (!snapshot || wc.isDestroyed()) return;
15169
+ async function restoreLocaleSnapshot(wc, snapshot2) {
15170
+ if (!snapshot2 || wc.isDestroyed()) return;
15014
15171
  try {
15015
15172
  if (typeof wc.canGoBack === "function" && wc.canGoBack()) {
15016
15173
  wc.goBack();
15017
15174
  await waitForLoad(wc, 3e3);
15018
15175
  const reverted = await getLocaleSnapshot(wc);
15019
- if (!localeChanged(snapshot, reverted)) {
15176
+ if (!localeChanged(snapshot2, reverted)) {
15020
15177
  return;
15021
15178
  }
15022
15179
  }
15023
15180
  } catch (err) {
15024
15181
  logger$c.warn("Failed to restore locale via history navigation, trying URL reload fallback:", err);
15025
15182
  }
15026
- if (snapshot.url && snapshot.url !== wc.getURL()) {
15183
+ if (snapshot2.url && snapshot2.url !== wc.getURL()) {
15027
15184
  try {
15028
- assertSafeURL(snapshot.url);
15029
- await wc.loadURL(snapshot.url);
15185
+ assertSafeURL(snapshot2.url);
15186
+ await wc.loadURL(snapshot2.url);
15030
15187
  await waitForLoad(wc, 3e3);
15031
15188
  return;
15032
15189
  } catch (err) {
15033
15190
  logger$c.warn("Failed to restore locale via safe URL load, trying page reload fallback:", err);
15034
15191
  }
15035
15192
  }
15036
- if (snapshot.url) {
15193
+ if (snapshot2.url) {
15037
15194
  try {
15038
15195
  await wc.reload();
15039
15196
  await waitForLoad(wc, 3e3);
@@ -18983,9 +19140,9 @@ function capturePageToVault({
18983
19140
  if (page.excerpt.trim()) {
18984
19141
  bodyLines.push("## Excerpt", "", page.excerpt.trim(), "");
18985
19142
  }
18986
- const snapshot = trimContent(page.content);
18987
- if (snapshot) {
18988
- bodyLines.push("## Page Snapshot", "", snapshot, "");
19143
+ const snapshot2 = trimContent(page.content);
19144
+ if (snapshot2) {
19145
+ bodyLines.push("## Page Snapshot", "", snapshot2, "");
18989
19146
  }
18990
19147
  return writeMemoryNote({
18991
19148
  title: noteTitle,
@@ -19410,8 +19567,8 @@ Exception: ${result.exceptionDetails}`);
19410
19567
  {},
19411
19568
  async () => {
19412
19569
  const session = getOrCreateSession(tabManager);
19413
- const snapshot = await session.getPerformanceSnapshot();
19414
- return JSON.stringify(snapshot, null, 2);
19570
+ const snapshot2 = await session.getPerformanceSnapshot();
19571
+ return JSON.stringify(snapshot2, null, 2);
19415
19572
  }
19416
19573
  );
19417
19574
  }
@@ -19469,6 +19626,16 @@ const logger$b = createLogger("VaultShared");
19469
19626
  const ALGORITHM = "aes-256-gcm";
19470
19627
  const IV_LENGTH = 12;
19471
19628
  const AUTH_TAG_LENGTH = 16;
19629
+ const KEY_STORAGE_PREFIX = "base64:";
19630
+ function encodeEncryptionKeyForStorage(key2) {
19631
+ return `${KEY_STORAGE_PREFIX}${key2.toString("base64")}`;
19632
+ }
19633
+ function decodeEncryptionKeyFromStorage(value) {
19634
+ if (value.startsWith(KEY_STORAGE_PREFIX)) {
19635
+ return Buffer.from(value.slice(KEY_STORAGE_PREFIX.length), "base64");
19636
+ }
19637
+ return Buffer.from(value, "utf-8");
19638
+ }
19472
19639
  function assertSecretStorageAvailable(customMessage) {
19473
19640
  if (!electron.safeStorage.isEncryptionAvailable()) {
19474
19641
  throw new Error(
@@ -19481,12 +19648,19 @@ function getOrCreateEncryptionKey(keyFilename) {
19481
19648
  const keyPath = path$1.join(electron.app.getPath("userData"), keyFilename);
19482
19649
  if (fs$1.existsSync(keyPath)) {
19483
19650
  const encryptedKey = fs$1.readFileSync(keyPath);
19484
- return Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8");
19651
+ const key22 = decodeEncryptionKeyFromStorage(
19652
+ electron.safeStorage.decryptString(encryptedKey)
19653
+ );
19654
+ if (key22.length !== 32) {
19655
+ throw new Error("Stored vault encryption key has an invalid length.");
19656
+ }
19657
+ return key22;
19485
19658
  }
19486
19659
  const key2 = crypto$2.randomBytes(32);
19487
19660
  fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
19488
- const encrypted = electron.safeStorage.encryptString(key2.toString("utf-8"));
19661
+ const encrypted = electron.safeStorage.encryptString(encodeEncryptionKeyForStorage(key2));
19489
19662
  fs$1.writeFileSync(keyPath, encrypted, { mode: 384 });
19663
+ fs$1.chmodSync(keyPath, 384);
19490
19664
  return key2;
19491
19665
  }
19492
19666
  function createEncryptDecrypt(keyFilename) {
@@ -19551,7 +19725,7 @@ function createVaultIO(vaultFilename, encrypt2, decrypt2) {
19551
19725
  const encrypted = encrypt2(json);
19552
19726
  const vaultPath = getVaultPath();
19553
19727
  fs$1.mkdirSync(path$1.dirname(vaultPath), { recursive: true });
19554
- fs$1.writeFileSync(vaultPath, encrypted);
19728
+ fs$1.writeFileSync(vaultPath, encrypted, { mode: 384 });
19555
19729
  fs$1.chmodSync(vaultPath, 384);
19556
19730
  cachedEntries = entries;
19557
19731
  }
@@ -19560,15 +19734,21 @@ function createVaultIO(vaultFilename, encrypt2, decrypt2) {
19560
19734
  }
19561
19735
  return { loadVault: loadVault2, saveVault: saveVault2, resetCache };
19562
19736
  }
19563
- function domainMatches(pattern, hostname) {
19564
- const p = pattern.toLowerCase().trim();
19565
- const h = hostname.toLowerCase().trim();
19566
- if (p === h) return true;
19567
- if (p.startsWith("*.")) {
19568
- const suffix = p.slice(2);
19569
- return h === suffix || h.endsWith("." + suffix);
19737
+ function normalizeCredentialHost(value) {
19738
+ try {
19739
+ const parsed = new URL(value.includes("://") ? value : `https://${value}`);
19740
+ return parsed.hostname.toLowerCase().replace(/^www\./, "");
19741
+ } catch {
19742
+ const normalized = value.toLowerCase().trim().replace(/^(https?:\/\/)?(www\.)?/, "").replace(/\/.*$/, "");
19743
+ return normalized && !normalized.includes(" ") ? normalized : null;
19570
19744
  }
19571
- return false;
19745
+ }
19746
+ function domainMatches(pattern, hostname) {
19747
+ const isWildcard = pattern.trim().startsWith("*.");
19748
+ const p = normalizeCredentialHost(isWildcard ? pattern.slice(2) : pattern);
19749
+ const h = normalizeCredentialHost(hostname);
19750
+ if (!p || !h) return false;
19751
+ return isWildcard ? h.endsWith("." + p) : p === h;
19572
19752
  }
19573
19753
  function generateTotpCode(secret) {
19574
19754
  const epoch = Math.floor(Date.now() / 1e3);
@@ -19601,12 +19781,20 @@ function createAuditLog(filename, maxEntries) {
19601
19781
  try {
19602
19782
  const auditPath = getAuditPath2();
19603
19783
  fs$1.mkdirSync(path$1.dirname(auditPath), { recursive: true });
19604
- fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n");
19784
+ fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n", {
19785
+ encoding: "utf-8",
19786
+ mode: 384
19787
+ });
19788
+ fs$1.chmodSync(auditPath, 384);
19605
19789
  try {
19606
19790
  const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
19607
19791
  if (lines.length > maxEntries) {
19608
19792
  const trimmed = lines.slice(-maxEntries);
19609
- fs$1.writeFileSync(auditPath, trimmed.join("\n") + "\n");
19793
+ fs$1.writeFileSync(auditPath, trimmed.join("\n") + "\n", {
19794
+ encoding: "utf-8",
19795
+ mode: 384
19796
+ });
19797
+ fs$1.chmodSync(auditPath, 384);
19610
19798
  }
19611
19799
  } catch {
19612
19800
  }
@@ -19639,12 +19827,8 @@ function listEntries$1() {
19639
19827
  return loadVault$1().map(({ password, totpSecret, ...rest }) => rest);
19640
19828
  }
19641
19829
  function findEntriesForDomain(url) {
19642
- let hostname;
19643
- try {
19644
- hostname = new URL(url).hostname;
19645
- } catch {
19646
- return [];
19647
- }
19830
+ const hostname = normalizeCredentialHost(url);
19831
+ if (!hostname) return [];
19648
19832
  return loadVault$1().filter((e) => domainMatches(e.domainPattern, hostname)).map(({ password, totpSecret, ...rest }) => rest);
19649
19833
  }
19650
19834
  function addEntry(entry) {
@@ -19737,7 +19921,11 @@ function appendAuditEntry(entry) {
19737
19921
  try {
19738
19922
  const auditPath = getAuditPath();
19739
19923
  fs$1.mkdirSync(path$1.dirname(auditPath), { recursive: true });
19740
- fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n");
19924
+ fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n", {
19925
+ encoding: "utf-8",
19926
+ mode: 384
19927
+ });
19928
+ fs$1.chmodSync(auditPath, 384);
19741
19929
  } catch (err) {
19742
19930
  logger$a.error("Failed to write audit log:", err);
19743
19931
  }
@@ -19767,12 +19955,7 @@ const auditLog = createAuditLog(
19767
19955
  AUDIT_MAX_ENTRIES
19768
19956
  );
19769
19957
  function extractDomain(url) {
19770
- try {
19771
- const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
19772
- return parsed.hostname.toLowerCase();
19773
- } catch {
19774
- return url.toLowerCase().replace(/^(https?:\/\/)?(www\.)?/, "").replace(/\/.*$/, "");
19775
- }
19958
+ return normalizeCredentialHost(url) ?? "";
19776
19959
  }
19777
19960
  function listEntries() {
19778
19961
  return loadVault().map(({ password, totpSecret, ...rest }) => rest);
@@ -19957,6 +20140,7 @@ function writeMcpAuthFile(endpoint, token) {
19957
20140
  JSON.stringify({ endpoint, token, pid: process.pid }, null, 2) + "\n",
19958
20141
  { mode: 384 }
19959
20142
  );
20143
+ fs$1.chmodSync(filePath2, 384);
19960
20144
  } catch (err) {
19961
20145
  logger$9.warn("Failed to write auth file:", err);
19962
20146
  }
@@ -19982,10 +20166,18 @@ function clearMcpAuthFile() {
19982
20166
  ) + "\n",
19983
20167
  { mode: 384 }
19984
20168
  );
20169
+ fs$1.chmodSync(filePath2, 384);
19985
20170
  } catch (err) {
19986
20171
  logger$9.warn("Failed to clear auth file:", err);
19987
20172
  }
19988
20173
  }
20174
+ function regenerateMcpAuthToken() {
20175
+ const endpoint = getRuntimeHealth().mcp.endpoint;
20176
+ if (!httpServer || !endpoint) return null;
20177
+ mcpAuthToken = crypto$2.randomBytes(32).toString("hex");
20178
+ writeMcpAuthFile(endpoint, mcpAuthToken);
20179
+ return { endpoint };
20180
+ }
19989
20181
  function asTextResponse(text) {
19990
20182
  return { content: [{ type: "text", text }] };
19991
20183
  }
@@ -20011,6 +20203,11 @@ function asPromptResponse(text) {
20011
20203
  function isDangerousMcpAction(name) {
20012
20204
  return name === "close_tab" || isDangerousAction(name);
20013
20205
  }
20206
+ function requiresExplicitMcpApproval(name, args) {
20207
+ if (name === "delete_session" || name === "close_tab" || name === "load_session") return true;
20208
+ if (name === "remove_bookmark_folder" && args.delete_contents === true) return true;
20209
+ return false;
20210
+ }
20014
20211
  function getActiveTabSummary(tabManager) {
20015
20212
  const activeTab = tabManager.getActiveTab();
20016
20213
  const activeTabId = tabManager.getActiveTabId();
@@ -20099,6 +20296,7 @@ async function withAction(runtime2, tabManager, name, args, executor) {
20099
20296
  args,
20100
20297
  tabId: tabManager.getActiveTabId(),
20101
20298
  dangerous: isDangerousMcpAction(name),
20299
+ requiresApproval: requiresExplicitMcpApproval(name, args),
20102
20300
  executor
20103
20301
  });
20104
20302
  const stateInfo = await getPostActionState(tabManager, name);
@@ -23841,6 +24039,43 @@ function uninstallKit(id, scheduledKitIds) {
23841
24039
  return errorResult("Failed to remove the kit file.");
23842
24040
  }
23843
24041
  }
24042
+ const trustedIpcSenderIds = /* @__PURE__ */ new Set();
24043
+ function registerTrustedIpcSender(wc) {
24044
+ trustedIpcSenderIds.add(wc.id);
24045
+ wc.once("destroyed", () => trustedIpcSenderIds.delete(wc.id));
24046
+ }
24047
+ function assertTrustedIpcSender(event) {
24048
+ if (!trustedIpcSenderIds.has(event.sender.id)) {
24049
+ throw new Error("Blocked IPC from untrusted renderer");
24050
+ }
24051
+ }
24052
+ function isManagedTabIpcSender(event, tabManager) {
24053
+ return Boolean(tabManager.findTabByWebContentsId(event.sender.id));
24054
+ }
24055
+ function assertString(value, name) {
24056
+ if (typeof value !== "string") throw new Error(`${name} must be a string`);
24057
+ }
24058
+ function assertOptionalString(value, name) {
24059
+ if (value !== void 0 && typeof value !== "string") {
24060
+ throw new Error(`${name} must be a string`);
24061
+ }
24062
+ }
24063
+ function assertNumber(value, name) {
24064
+ if (typeof value !== "number" || Number.isNaN(value)) {
24065
+ throw new Error(`${name} must be a number`);
24066
+ }
24067
+ }
24068
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
24069
+ function isValidEmail(value) {
24070
+ return EMAIL_RE.test(value.trim());
24071
+ }
24072
+ function getActiveTabInfo(tabManager) {
24073
+ const tab = tabManager.getActiveTab();
24074
+ if (!tab) return null;
24075
+ const wc = tab.view.webContents;
24076
+ if (wc.isDestroyed()) return null;
24077
+ return { tab, wc };
24078
+ }
23844
24079
  const logger$7 = createLogger("Scheduler");
23845
24080
  let jobs = [];
23846
24081
  let removeIdleListener = null;
@@ -23864,7 +24099,13 @@ function loadJobs() {
23864
24099
  }
23865
24100
  function saveJobs() {
23866
24101
  try {
23867
- fs$1.writeFileSync(getJobsPath(), JSON.stringify(jobs, null, 2), "utf-8");
24102
+ const jobsPath = getJobsPath();
24103
+ fs$1.mkdirSync(path$1.dirname(jobsPath), { recursive: true });
24104
+ fs$1.writeFileSync(jobsPath, JSON.stringify(jobs, null, 2), {
24105
+ encoding: "utf-8",
24106
+ mode: 384
24107
+ });
24108
+ fs$1.chmodSync(jobsPath, 384);
23868
24109
  } catch (err) {
23869
24110
  logger$7.warn("Failed to save jobs:", err);
23870
24111
  }
@@ -24068,8 +24309,12 @@ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
24068
24309
  tick(windowState, runtime2);
24069
24310
  setInterval(() => tick(windowState, runtime2), 6e4);
24070
24311
  }, msToNextMinute);
24071
- electron.ipcMain.handle(Channels.SCHEDULE_GET_ALL, () => jobs);
24072
- electron.ipcMain.handle(Channels.SCHEDULE_CREATE, (_, rawJob) => {
24312
+ electron.ipcMain.handle(Channels.SCHEDULE_GET_ALL, (event) => {
24313
+ assertTrustedIpcSender(event);
24314
+ return jobs;
24315
+ });
24316
+ electron.ipcMain.handle(Channels.SCHEDULE_CREATE, (event, rawJob) => {
24317
+ assertTrustedIpcSender(event);
24073
24318
  if (!isValidJobData(rawJob)) {
24074
24319
  throw new Error(
24075
24320
  "Invalid job data. Required: kitId, kitName, kitIcon, renderedPrompt, schedule, enabled."
@@ -24086,7 +24331,8 @@ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
24086
24331
  sendToAll(Channels.SCHEDULE_JOBS_UPDATE, jobs);
24087
24332
  return newJob;
24088
24333
  });
24089
- electron.ipcMain.handle(Channels.SCHEDULE_UPDATE, (_, id, updates) => {
24334
+ electron.ipcMain.handle(Channels.SCHEDULE_UPDATE, (event, id, updates) => {
24335
+ assertTrustedIpcSender(event);
24090
24336
  if (typeof id !== "string") throw new Error("id must be a string");
24091
24337
  const job = jobs.find((j) => j.id === id);
24092
24338
  if (!job) return null;
@@ -24112,7 +24358,8 @@ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
24112
24358
  sendToAll(Channels.SCHEDULE_JOBS_UPDATE, jobs);
24113
24359
  return job;
24114
24360
  });
24115
- electron.ipcMain.handle(Channels.SCHEDULE_DELETE, (_, id) => {
24361
+ electron.ipcMain.handle(Channels.SCHEDULE_DELETE, (event, id) => {
24362
+ assertTrustedIpcSender(event);
24116
24363
  if (typeof id !== "string") throw new Error("id must be a string");
24117
24364
  const before = jobs.length;
24118
24365
  jobs = jobs.filter((j) => j.id !== id);
@@ -24122,30 +24369,6 @@ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
24122
24369
  return true;
24123
24370
  });
24124
24371
  }
24125
- function assertString(value, name) {
24126
- if (typeof value !== "string") throw new Error(`${name} must be a string`);
24127
- }
24128
- function assertOptionalString(value, name) {
24129
- if (value !== void 0 && typeof value !== "string") {
24130
- throw new Error(`${name} must be a string`);
24131
- }
24132
- }
24133
- function assertNumber(value, name) {
24134
- if (typeof value !== "number" || Number.isNaN(value)) {
24135
- throw new Error(`${name} must be a number`);
24136
- }
24137
- }
24138
- const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
24139
- function isValidEmail(value) {
24140
- return EMAIL_RE.test(value.trim());
24141
- }
24142
- function getActiveTabInfo(tabManager) {
24143
- const tab = tabManager.getActiveTab();
24144
- if (!tab) return null;
24145
- const wc = tab.view.webContents;
24146
- if (wc.isDestroyed()) return null;
24147
- return { tab, wc };
24148
- }
24149
24372
  const SAVE_DEBOUNCE_MS = 250;
24150
24373
  const PROFILE_FIELDS = [
24151
24374
  "label",
@@ -24485,24 +24708,29 @@ function sanitizeAutofillUpdates(value) {
24485
24708
  return updates;
24486
24709
  }
24487
24710
  function registerAutofillHandlers(windowState) {
24488
- electron.ipcMain.handle(Channels.AUTOFILL_LIST, () => {
24711
+ electron.ipcMain.handle(Channels.AUTOFILL_LIST, (event) => {
24712
+ assertTrustedIpcSender(event);
24489
24713
  return listProfiles();
24490
24714
  });
24491
24715
  electron.ipcMain.handle(
24492
24716
  Channels.AUTOFILL_ADD,
24493
- (_, profile) => {
24717
+ (event, profile) => {
24718
+ assertTrustedIpcSender(event);
24494
24719
  return addProfile(sanitizeAutofillProfile(profile));
24495
24720
  }
24496
24721
  );
24497
- electron.ipcMain.handle(Channels.AUTOFILL_UPDATE, (_, id, updates) => {
24722
+ electron.ipcMain.handle(Channels.AUTOFILL_UPDATE, (event, id, updates) => {
24723
+ assertTrustedIpcSender(event);
24498
24724
  assertString(id, "id");
24499
24725
  return updateProfile(id, sanitizeAutofillUpdates(updates));
24500
24726
  });
24501
- electron.ipcMain.handle(Channels.AUTOFILL_DELETE, (_, id) => {
24727
+ electron.ipcMain.handle(Channels.AUTOFILL_DELETE, (event, id) => {
24728
+ assertTrustedIpcSender(event);
24502
24729
  assertString(id, "id");
24503
24730
  return deleteProfile(id);
24504
24731
  });
24505
- electron.ipcMain.handle(Channels.AUTOFILL_FILL, async (_, profileId) => {
24732
+ electron.ipcMain.handle(Channels.AUTOFILL_FILL, async (event, profileId) => {
24733
+ assertTrustedIpcSender(event);
24506
24734
  assertString(profileId, "profileId");
24507
24735
  const profile = getProfile(profileId);
24508
24736
  if (!profile) throw new Error("Profile not found");
@@ -24537,13 +24765,26 @@ function registerAutofillHandlers(windowState) {
24537
24765
  });
24538
24766
  }
24539
24767
  function registerPageDiffHandlers(windowState, sendToRendererViews) {
24540
- electron.ipcMain.handle(Channels.PAGE_DIFF_GET, () => {
24768
+ const pageEventBuckets = /* @__PURE__ */ new Map();
24769
+ const allowPageEvent = (webContentsId) => {
24770
+ const now = Date.now();
24771
+ const bucket = pageEventBuckets.get(webContentsId);
24772
+ if (!bucket || bucket.resetAt <= now) {
24773
+ pageEventBuckets.set(webContentsId, { count: 1, resetAt: now + 1e3 });
24774
+ return true;
24775
+ }
24776
+ bucket.count += 1;
24777
+ return bucket.count <= 20;
24778
+ };
24779
+ electron.ipcMain.handle(Channels.PAGE_DIFF_GET, (event) => {
24780
+ assertTrustedIpcSender(event);
24541
24781
  const activeTab = windowState.tabManager.getActiveTab();
24542
24782
  const wc = activeTab?.view.webContents;
24543
24783
  if (!wc) return null;
24544
24784
  return getLatestPageDiff(wc.getURL());
24545
24785
  });
24546
- electron.ipcMain.handle(Channels.PAGE_DIFF_HISTORY, () => {
24786
+ electron.ipcMain.handle(Channels.PAGE_DIFF_HISTORY, (event) => {
24787
+ assertTrustedIpcSender(event);
24547
24788
  try {
24548
24789
  if (!isPremiumActiveState(getPremiumState())) {
24549
24790
  return { error: "Premium required" };
@@ -24559,21 +24800,27 @@ function registerPageDiffHandlers(windowState, sendToRendererViews) {
24559
24800
  electron.ipcMain.on(Channels.PAGE_DIFF_ACTIVITY, (event) => {
24560
24801
  const wc = event.sender;
24561
24802
  if (!wc || wc.isDestroyed()) return;
24803
+ if (!isManagedTabIpcSender(event, windowState.tabManager)) return;
24804
+ if (!allowPageEvent(wc.id)) return;
24562
24805
  notePageMutationActivity(wc, sendToRendererViews);
24563
24806
  });
24564
24807
  electron.ipcMain.on(Channels.PAGE_DIFF_DIRTY, (event) => {
24565
24808
  const wc = event.sender;
24566
24809
  if (!wc || wc.isDestroyed()) return;
24810
+ if (!isManagedTabIpcSender(event, windowState.tabManager)) return;
24811
+ if (!allowPageEvent(wc.id)) return;
24567
24812
  schedulePageSnapshotCapture(wc, sendToRendererViews);
24568
24813
  });
24569
24814
  }
24570
24815
  function registerVaultHandlers() {
24571
- electron.ipcMain.handle(Channels.VAULT_LIST, () => {
24816
+ electron.ipcMain.handle(Channels.VAULT_LIST, (event) => {
24817
+ assertTrustedIpcSender(event);
24572
24818
  return listEntries$1();
24573
24819
  });
24574
24820
  electron.ipcMain.handle(
24575
24821
  Channels.VAULT_ADD,
24576
- (_, entry) => {
24822
+ (event, entry) => {
24823
+ assertTrustedIpcSender(event);
24577
24824
  if (!entry || typeof entry !== "object") {
24578
24825
  throw new Error("Invalid vault entry");
24579
24826
  }
@@ -24598,7 +24845,8 @@ function registerVaultHandlers() {
24598
24845
  );
24599
24846
  electron.ipcMain.handle(
24600
24847
  Channels.VAULT_UPDATE,
24601
- (_, id, updates) => {
24848
+ (event, id, updates) => {
24849
+ assertTrustedIpcSender(event);
24602
24850
  assertString(id, "id");
24603
24851
  if (!updates || typeof updates !== "object") {
24604
24852
  throw new Error("Invalid updates");
@@ -24606,12 +24854,14 @@ function registerVaultHandlers() {
24606
24854
  return updateEntry$1(id, updates) !== null;
24607
24855
  }
24608
24856
  );
24609
- electron.ipcMain.handle(Channels.VAULT_REMOVE, (_, id) => {
24857
+ electron.ipcMain.handle(Channels.VAULT_REMOVE, (event, id) => {
24858
+ assertTrustedIpcSender(event);
24610
24859
  assertString(id, "id");
24611
24860
  trackVaultAction("credential_removed");
24612
24861
  return removeEntry$1(id);
24613
24862
  });
24614
- electron.ipcMain.handle(Channels.VAULT_AUDIT_LOG, (_, limit) => {
24863
+ electron.ipcMain.handle(Channels.VAULT_AUDIT_LOG, (event, limit) => {
24864
+ assertTrustedIpcSender(event);
24615
24865
  return readAuditLog$1(limit);
24616
24866
  });
24617
24867
  }
@@ -24630,17 +24880,20 @@ function normalizeTags(value) {
24630
24880
  return tags.length > 0 ? tags : void 0;
24631
24881
  }
24632
24882
  function registerHumanVaultHandlers() {
24633
- electron.ipcMain.handle(Channels.HUMAN_VAULT_LIST, (_, domain) => {
24883
+ electron.ipcMain.handle(Channels.HUMAN_VAULT_LIST, (event, domain) => {
24884
+ assertTrustedIpcSender(event);
24634
24885
  if (domain !== void 0) assertString(domain, "domain");
24635
24886
  return domain ? findForDomain(domain) : listEntries();
24636
24887
  });
24637
- electron.ipcMain.handle(Channels.HUMAN_VAULT_GET, (_, id) => {
24888
+ electron.ipcMain.handle(Channels.HUMAN_VAULT_GET, (event, id) => {
24889
+ assertTrustedIpcSender(event);
24638
24890
  assertString(id, "id");
24639
24891
  return getEntrySafe(id);
24640
24892
  });
24641
24893
  electron.ipcMain.handle(
24642
24894
  Channels.HUMAN_VAULT_SAVE,
24643
- (_, input) => {
24895
+ (event, input) => {
24896
+ assertTrustedIpcSender(event);
24644
24897
  if (!input || typeof input !== "object") {
24645
24898
  throw new Error("Invalid credential entry");
24646
24899
  }
@@ -24670,7 +24923,8 @@ function registerHumanVaultHandlers() {
24670
24923
  );
24671
24924
  electron.ipcMain.handle(
24672
24925
  Channels.HUMAN_VAULT_UPDATE,
24673
- (_, id, updates) => {
24926
+ (event, id, updates) => {
24927
+ assertTrustedIpcSender(event);
24674
24928
  assertString(id, "id");
24675
24929
  if (!updates || typeof updates !== "object") {
24676
24930
  throw new Error("Invalid updates");
@@ -24712,26 +24966,31 @@ function registerHumanVaultHandlers() {
24712
24966
  return safe;
24713
24967
  }
24714
24968
  );
24715
- electron.ipcMain.handle(Channels.HUMAN_VAULT_REMOVE, (_, id) => {
24969
+ electron.ipcMain.handle(Channels.HUMAN_VAULT_REMOVE, (event, id) => {
24970
+ assertTrustedIpcSender(event);
24716
24971
  assertString(id, "id");
24717
24972
  return removeEntry(id);
24718
24973
  });
24719
- electron.ipcMain.handle(Channels.HUMAN_VAULT_AUDIT_LOG, (_, limit) => {
24974
+ electron.ipcMain.handle(Channels.HUMAN_VAULT_AUDIT_LOG, (event, limit) => {
24975
+ assertTrustedIpcSender(event);
24720
24976
  return readAuditLog(limit);
24721
24977
  });
24722
24978
  }
24723
24979
  function registerWindowControlHandlers(mainWindow) {
24724
- electron.ipcMain.handle(Channels.WINDOW_MINIMIZE, () => {
24980
+ electron.ipcMain.handle(Channels.WINDOW_MINIMIZE, (event) => {
24981
+ assertTrustedIpcSender(event);
24725
24982
  mainWindow.minimize();
24726
24983
  });
24727
- electron.ipcMain.handle(Channels.WINDOW_MAXIMIZE, () => {
24984
+ electron.ipcMain.handle(Channels.WINDOW_MAXIMIZE, (event) => {
24985
+ assertTrustedIpcSender(event);
24728
24986
  if (mainWindow.isMaximized()) {
24729
24987
  mainWindow.unmaximize();
24730
24988
  } else {
24731
24989
  mainWindow.maximize();
24732
24990
  }
24733
24991
  });
24734
- electron.ipcMain.handle(Channels.WINDOW_CLOSE, () => {
24992
+ electron.ipcMain.handle(Channels.WINDOW_CLOSE, (event) => {
24993
+ assertTrustedIpcSender(event);
24735
24994
  mainWindow.close();
24736
24995
  });
24737
24996
  }
@@ -24870,6 +25129,32 @@ function installAdBlockingForSession(ses, tabManager) {
24870
25129
  });
24871
25130
  }
24872
25131
  const filePath$1 = () => path$1.join(electron.app.getPath("userData"), "vessel-downloads.json");
25132
+ const EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
25133
+ ".appimage",
25134
+ ".bat",
25135
+ ".cmd",
25136
+ ".command",
25137
+ ".desktop",
25138
+ ".exe",
25139
+ ".msi",
25140
+ ".ps1",
25141
+ ".scr",
25142
+ ".sh"
25143
+ ]);
25144
+ function hasMisleadingDoubleExtension(filename) {
25145
+ return /\.(pdf|docx?|xlsx?|pptx?|png|jpe?g|gif|txt|zip)\.(exe|msi|bat|cmd|ps1|sh|scr|appimage)$/i.test(filename);
25146
+ }
25147
+ function isExecutableDownload(savePath) {
25148
+ return EXECUTABLE_EXTENSIONS.has(path$1.extname(savePath).toLowerCase());
25149
+ }
25150
+ function executableWarningDetail(item) {
25151
+ return [
25152
+ "This file can run code on your computer. Only open it if you trust the source.",
25153
+ item.url ? `Source: ${item.url}` : null,
25154
+ item.mimeType ? `Type: ${item.mimeType}` : null,
25155
+ hasMisleadingDoubleExtension(item.filename) ? "Warning: this filename uses a misleading double extension." : null
25156
+ ].filter(Boolean).join("\n");
25157
+ }
24873
25158
  function parse(raw) {
24874
25159
  if (!raw || typeof raw !== "object") return { items: [] };
24875
25160
  const items = Array.isArray(raw.items) ? raw.items : [];
@@ -24888,13 +25173,13 @@ function persist() {
24888
25173
  persistence$1.schedule();
24889
25174
  }
24890
25175
  function emit$1() {
24891
- broadcaster$1?.(Channels.DOWNLOADS_UPDATE, state.items);
25176
+ broadcaster$1?.(Channels.DOWNLOADS_UPDATE, listDownloads());
24892
25177
  }
24893
25178
  function setDownloadBroadcaster(fn) {
24894
25179
  broadcaster$1 = fn;
24895
25180
  }
24896
25181
  function listDownloads() {
24897
- return state.items;
25182
+ return state.items.map((item) => ({ ...item }));
24898
25183
  }
24899
25184
  function upsertDownload(input) {
24900
25185
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -24918,7 +25203,19 @@ function clearDownloads() {
24918
25203
  }
24919
25204
  async function openDownload(id) {
24920
25205
  const item = state.items.find((d) => d.id === id);
24921
- if (!item || !fs$1.existsSync(item.savePath)) return false;
25206
+ if (!item || item.state !== "completed" || !fs$1.existsSync(item.savePath)) return false;
25207
+ if (isExecutableDownload(item.savePath)) {
25208
+ const result = electron.dialog.showMessageBoxSync({
25209
+ type: "warning",
25210
+ buttons: ["Cancel", "Open Anyway"],
25211
+ defaultId: 0,
25212
+ cancelId: 0,
25213
+ title: "Open executable download?",
25214
+ message: `Open ${item.filename}?`,
25215
+ detail: executableWarningDetail(item)
25216
+ });
25217
+ if (result !== 1) return false;
25218
+ }
24922
25219
  return await electron.shell.openPath(item.savePath) === "";
24923
25220
  }
24924
25221
  async function showDownloadInFolder(id) {
@@ -24969,6 +25266,8 @@ function installDownloadHandlerForSession(targetSession, chromeView) {
24969
25266
  const info = {
24970
25267
  filename,
24971
25268
  savePath,
25269
+ url: item.getURL(),
25270
+ mimeType: typeof item.getMimeType === "function" ? item.getMimeType() : void 0,
24972
25271
  totalBytes: item.getTotalBytes(),
24973
25272
  receivedBytes: 0,
24974
25273
  state: "progressing"
@@ -25016,9 +25315,9 @@ function loadRenderers(chromeView, sidebarView, devtoolsPanelView) {
25016
25315
  const sidebarUrl = rendererUrlFor("sidebar");
25017
25316
  const devtoolsUrl = rendererUrlFor("devtools");
25018
25317
  if (chromeUrl && sidebarUrl && devtoolsUrl) {
25019
- chromeView.webContents.loadURL(chromeUrl);
25020
- sidebarView.webContents.loadURL(sidebarUrl);
25021
- devtoolsPanelView.webContents.loadURL(devtoolsUrl);
25318
+ void loadTrustedAppURL(chromeView.webContents, chromeUrl);
25319
+ void loadTrustedAppURL(sidebarView.webContents, sidebarUrl);
25320
+ void loadTrustedAppURL(devtoolsPanelView.webContents, devtoolsUrl);
25022
25321
  } else {
25023
25322
  const rendererFile = resolveRendererFile();
25024
25323
  chromeView.webContents.loadFile(rendererFile, {
@@ -25244,7 +25543,7 @@ function loadPrivateRenderer(chromeView) {
25244
25543
  const url = new URL(devUrl);
25245
25544
  url.searchParams.set("view", "chrome");
25246
25545
  url.searchParams.set("private", "1");
25247
- chromeView.webContents.loadURL(url.toString());
25546
+ void loadTrustedAppURL(chromeView.webContents, url.toString());
25248
25547
  } else {
25249
25548
  chromeView.webContents.loadFile(resolveRendererFile(), {
25250
25549
  query: { view: "chrome", private: "1" }
@@ -25479,7 +25778,7 @@ function loadSecondaryRenderer(chromeView) {
25479
25778
  const url = new URL(devUrl);
25480
25779
  url.searchParams.set("view", "chrome");
25481
25780
  url.searchParams.set("secondary", "1");
25482
- chromeView.webContents.loadURL(url.toString());
25781
+ void loadTrustedAppURL(chromeView.webContents, url.toString());
25483
25782
  } else {
25484
25783
  chromeView.webContents.loadFile(resolveRendererFile(), {
25485
25784
  query: { view: "chrome", secondary: "1" }
@@ -25655,19 +25954,22 @@ function getSafeBookmarkExportName(name) {
25655
25954
  return safeName || "folder";
25656
25955
  }
25657
25956
  function registerBookmarkHandlers() {
25658
- electron.ipcMain.handle(Channels.BOOKMARKS_GET, () => {
25957
+ electron.ipcMain.handle(Channels.BOOKMARKS_GET, (event) => {
25958
+ assertTrustedIpcSender(event);
25659
25959
  return getState();
25660
25960
  });
25661
25961
  electron.ipcMain.handle(
25662
25962
  Channels.FOLDER_CREATE,
25663
- (_, name, summary) => {
25963
+ (event, name, summary) => {
25964
+ assertTrustedIpcSender(event);
25664
25965
  trackBookmarkAction("folder_create");
25665
25966
  return createFolderWithSummary(name, summary);
25666
25967
  }
25667
25968
  );
25668
25969
  electron.ipcMain.handle(
25669
25970
  Channels.BOOKMARK_SAVE,
25670
- (_, url, title, folderId, note, intent, expectedContent, keyFields, agentHints) => {
25971
+ (event, url, title, folderId, note, intent, expectedContent, keyFields, agentHints) => {
25972
+ assertTrustedIpcSender(event);
25671
25973
  trackBookmarkAction("save");
25672
25974
  const result = saveBookmarkWithPolicy(url, title, folderId, note, {
25673
25975
  onDuplicate: "update",
@@ -25688,18 +25990,21 @@ function registerBookmarkHandlers() {
25688
25990
  );
25689
25991
  electron.ipcMain.handle(
25690
25992
  Channels.BOOKMARK_UPDATE,
25691
- (_, id, updates) => {
25993
+ (event, id, updates) => {
25994
+ assertTrustedIpcSender(event);
25692
25995
  trackBookmarkAction("save");
25693
25996
  return updateBookmark(id, updates);
25694
25997
  }
25695
25998
  );
25696
- electron.ipcMain.handle(Channels.BOOKMARK_REMOVE, (_, id) => {
25999
+ electron.ipcMain.handle(Channels.BOOKMARK_REMOVE, (event, id) => {
26000
+ assertTrustedIpcSender(event);
25697
26001
  trackBookmarkAction("remove");
25698
26002
  return removeBookmark(id);
25699
26003
  });
25700
26004
  electron.ipcMain.handle(
25701
26005
  Channels.BOOKMARKS_EXPORT_HTML,
25702
- async (_, options) => {
26006
+ async (event, options) => {
26007
+ assertTrustedIpcSender(event);
25703
26008
  const { canceled, filePath: filePath2 } = await electron.dialog.showSaveDialog({
25704
26009
  title: "Export Bookmarks",
25705
26010
  defaultPath: "vessel-bookmarks.html",
@@ -25717,7 +26022,8 @@ function registerBookmarkHandlers() {
25717
26022
  };
25718
26023
  }
25719
26024
  );
25720
- electron.ipcMain.handle(Channels.BOOKMARKS_EXPORT_JSON, async () => {
26025
+ electron.ipcMain.handle(Channels.BOOKMARKS_EXPORT_JSON, async (event) => {
26026
+ assertTrustedIpcSender(event);
25721
26027
  const { canceled, filePath: filePath2 } = await electron.dialog.showSaveDialog({
25722
26028
  title: "Export Vessel Bookmark Archive",
25723
26029
  defaultPath: "vessel-bookmarks.json",
@@ -25734,7 +26040,8 @@ function registerBookmarkHandlers() {
25734
26040
  });
25735
26041
  electron.ipcMain.handle(
25736
26042
  Channels.FOLDER_EXPORT_HTML,
25737
- async (_, folderId, options) => {
26043
+ async (event, folderId, options) => {
26044
+ assertTrustedIpcSender(event);
25738
26045
  const folder = getFolder(folderId);
25739
26046
  if (!folder) return null;
25740
26047
  const { canceled, filePath: filePath2 } = await electron.dialog.showSaveDialog({
@@ -25755,7 +26062,8 @@ function registerBookmarkHandlers() {
25755
26062
  };
25756
26063
  }
25757
26064
  );
25758
- electron.ipcMain.handle(Channels.BOOKMARKS_IMPORT_HTML, async () => {
26065
+ electron.ipcMain.handle(Channels.BOOKMARKS_IMPORT_HTML, async (event) => {
26066
+ assertTrustedIpcSender(event);
25759
26067
  const { canceled, filePaths } = await electron.dialog.showOpenDialog({
25760
26068
  title: "Import Bookmarks",
25761
26069
  filters: [
@@ -25768,7 +26076,8 @@ function registerBookmarkHandlers() {
25768
26076
  trackBookmarkAction("import");
25769
26077
  return importBookmarksFromHtml(content);
25770
26078
  });
25771
- electron.ipcMain.handle(Channels.BOOKMARKS_IMPORT_JSON, async () => {
26079
+ electron.ipcMain.handle(Channels.BOOKMARKS_IMPORT_JSON, async (event) => {
26080
+ assertTrustedIpcSender(event);
25772
26081
  const { canceled, filePaths } = await electron.dialog.showOpenDialog({
25773
26082
  title: "Import Bookmarks",
25774
26083
  filters: [
@@ -25781,28 +26090,34 @@ function registerBookmarkHandlers() {
25781
26090
  trackBookmarkAction("import");
25782
26091
  return importBookmarksFromJson(content);
25783
26092
  });
25784
- electron.ipcMain.handle(Channels.FOLDER_REMOVE, (_, id, deleteContents) => {
26093
+ electron.ipcMain.handle(Channels.FOLDER_REMOVE, (event, id, deleteContents) => {
26094
+ assertTrustedIpcSender(event);
25785
26095
  trackBookmarkAction("folder_remove");
25786
26096
  return removeFolder(id, deleteContents ?? false);
25787
26097
  });
25788
26098
  electron.ipcMain.handle(
25789
26099
  Channels.FOLDER_RENAME,
25790
- (_, id, newName, summary) => {
26100
+ (event, id, newName, summary) => {
26101
+ assertTrustedIpcSender(event);
25791
26102
  return renameFolder(id, newName, summary);
25792
26103
  }
25793
26104
  );
25794
26105
  }
25795
26106
  function registerHistoryHandlers() {
25796
- electron.ipcMain.handle(Channels.HISTORY_GET, () => {
26107
+ electron.ipcMain.handle(Channels.HISTORY_GET, (event) => {
26108
+ assertTrustedIpcSender(event);
25797
26109
  return getState$1();
25798
26110
  });
25799
- electron.ipcMain.handle(Channels.HISTORY_SEARCH, (_, query) => {
26111
+ electron.ipcMain.handle(Channels.HISTORY_SEARCH, (event, query) => {
26112
+ assertTrustedIpcSender(event);
25800
26113
  return search(query);
25801
26114
  });
25802
- electron.ipcMain.handle(Channels.HISTORY_CLEAR, () => {
26115
+ electron.ipcMain.handle(Channels.HISTORY_CLEAR, (event) => {
26116
+ assertTrustedIpcSender(event);
25803
26117
  clearAll$1();
25804
26118
  });
25805
- electron.ipcMain.handle(Channels.HISTORY_EXPORT_HTML, async () => {
26119
+ electron.ipcMain.handle(Channels.HISTORY_EXPORT_HTML, async (event) => {
26120
+ assertTrustedIpcSender(event);
25806
26121
  const { canceled, filePath: filePath2 } = await electron.dialog.showSaveDialog({
25807
26122
  title: "Export History",
25808
26123
  defaultPath: "vessel-history.html",
@@ -25813,7 +26128,8 @@ function registerHistoryHandlers() {
25813
26128
  await fs.promises.writeFile(filePath2, content, "utf-8");
25814
26129
  return { filePath: filePath2, count: getState$1().entries.length };
25815
26130
  });
25816
- electron.ipcMain.handle(Channels.HISTORY_EXPORT_JSON, async () => {
26131
+ electron.ipcMain.handle(Channels.HISTORY_EXPORT_JSON, async (event) => {
26132
+ assertTrustedIpcSender(event);
25817
26133
  const { canceled, filePath: filePath2 } = await electron.dialog.showSaveDialog({
25818
26134
  title: "Export History",
25819
26135
  defaultPath: "vessel-history.json",
@@ -25824,7 +26140,8 @@ function registerHistoryHandlers() {
25824
26140
  await fs.promises.writeFile(filePath2, content, "utf-8");
25825
26141
  return { filePath: filePath2, count: getState$1().entries.length };
25826
26142
  });
25827
- electron.ipcMain.handle(Channels.HISTORY_IMPORT, async () => {
26143
+ electron.ipcMain.handle(Channels.HISTORY_IMPORT, async (event) => {
26144
+ assertTrustedIpcSender(event);
25828
26145
  const { canceled, filePaths } = await electron.dialog.showOpenDialog({
25829
26146
  title: "Import History",
25830
26147
  filters: [
@@ -25917,10 +26234,12 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
25917
26234
  void handleUrl(currentUrl);
25918
26235
  }
25919
26236
  };
25920
- electron.ipcMain.handle(Channels.PREMIUM_GET_STATE, () => {
26237
+ electron.ipcMain.handle(Channels.PREMIUM_GET_STATE, (event) => {
26238
+ assertTrustedIpcSender(event);
25921
26239
  return getPremiumState();
25922
26240
  });
25923
- electron.ipcMain.handle(Channels.PREMIUM_ACTIVATION_START, async (_, email) => {
26241
+ electron.ipcMain.handle(Channels.PREMIUM_ACTIVATION_START, async (event, email) => {
26242
+ assertTrustedIpcSender(event);
25924
26243
  assertString(email, "email");
25925
26244
  if (!isValidEmail(email)) {
25926
26245
  return errorResult("Invalid email format");
@@ -25934,7 +26253,8 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
25934
26253
  });
25935
26254
  electron.ipcMain.handle(
25936
26255
  Channels.PREMIUM_ACTIVATION_VERIFY,
25937
- async (_, email, code, challengeToken) => {
26256
+ async (event, email, code, challengeToken) => {
26257
+ assertTrustedIpcSender(event);
25938
26258
  assertString(email, "email");
25939
26259
  assertString(code, "code");
25940
26260
  assertString(challengeToken, "challengeToken");
@@ -25956,7 +26276,8 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
25956
26276
  return result;
25957
26277
  }
25958
26278
  );
25959
- electron.ipcMain.handle(Channels.PREMIUM_CHECKOUT, async (_, email) => {
26279
+ electron.ipcMain.handle(Channels.PREMIUM_CHECKOUT, async (event, email) => {
26280
+ assertTrustedIpcSender(event);
25960
26281
  trackPremiumFunnel("checkout_clicked");
25961
26282
  const result = await getCheckoutUrl(email);
25962
26283
  if (result.ok && result.url) {
@@ -25965,19 +26286,22 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
25965
26286
  }
25966
26287
  return result;
25967
26288
  });
25968
- electron.ipcMain.handle(Channels.PREMIUM_RESET, () => {
26289
+ electron.ipcMain.handle(Channels.PREMIUM_RESET, (event) => {
26290
+ assertTrustedIpcSender(event);
25969
26291
  trackPremiumFunnel("reset");
25970
26292
  const state2 = resetPremium();
25971
26293
  sendToRendererViews(Channels.PREMIUM_UPDATE, state2);
25972
26294
  return state2;
25973
26295
  });
25974
- electron.ipcMain.handle(Channels.PREMIUM_TRACK_CONTEXT, (_, step) => {
26296
+ electron.ipcMain.handle(Channels.PREMIUM_TRACK_CONTEXT, (event, step) => {
26297
+ assertTrustedIpcSender(event);
25975
26298
  assertString(step, "step");
25976
26299
  if (PREMIUM_TRACKABLE_STEPS.includes(step)) {
25977
26300
  trackPremiumFunnel(step);
25978
26301
  }
25979
26302
  });
25980
- electron.ipcMain.handle(Channels.PREMIUM_PORTAL, async () => {
26303
+ electron.ipcMain.handle(Channels.PREMIUM_PORTAL, async (event) => {
26304
+ assertTrustedIpcSender(event);
25981
26305
  trackPremiumFunnel("portal_opened");
25982
26306
  const result = await getPortalUrl();
25983
26307
  if (result.ok && result.url) {
@@ -25987,18 +26311,22 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
25987
26311
  });
25988
26312
  }
25989
26313
  function registerSessionHandlers(tabManager) {
25990
- electron.ipcMain.handle(Channels.SESSION_LIST, () => {
26314
+ electron.ipcMain.handle(Channels.SESSION_LIST, (event) => {
26315
+ assertTrustedIpcSender(event);
25991
26316
  return listNamedSessions();
25992
26317
  });
25993
- electron.ipcMain.handle(Channels.SESSION_SAVE, async (_, name) => {
26318
+ electron.ipcMain.handle(Channels.SESSION_SAVE, async (event, name) => {
26319
+ assertTrustedIpcSender(event);
25994
26320
  assertString(name, "name");
25995
26321
  return await saveNamedSession(tabManager, name);
25996
26322
  });
25997
- electron.ipcMain.handle(Channels.SESSION_LOAD, async (_, name) => {
26323
+ electron.ipcMain.handle(Channels.SESSION_LOAD, async (event, name) => {
26324
+ assertTrustedIpcSender(event);
25998
26325
  assertString(name, "name");
25999
26326
  return await loadNamedSession(tabManager, name);
26000
26327
  });
26001
- electron.ipcMain.handle(Channels.SESSION_DELETE, (_, name) => {
26328
+ electron.ipcMain.handle(Channels.SESSION_DELETE, (event, name) => {
26329
+ assertTrustedIpcSender(event);
26002
26330
  assertString(name, "name");
26003
26331
  return deleteNamedSession(name);
26004
26332
  });
@@ -26035,7 +26363,8 @@ function buildCertificateDetailsHtml(state2) {
26035
26363
  </html>`;
26036
26364
  }
26037
26365
  function registerSecurityHandlers(tabManager) {
26038
- electron.ipcMain.handle(Channels.SECURITY_SHOW_DETAILS, async (_, state2) => {
26366
+ electron.ipcMain.handle(Channels.SECURITY_SHOW_DETAILS, async (event, state2) => {
26367
+ assertTrustedIpcSender(event);
26039
26368
  const domain = (() => {
26040
26369
  try {
26041
26370
  return new URL(state2.url).hostname || state2.url;
@@ -26056,13 +26385,15 @@ function registerSecurityHandlers(tabManager) {
26056
26385
  spellcheck: false
26057
26386
  }
26058
26387
  });
26059
- void win.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(content)}`);
26388
+ void loadInternalDataURL(win.webContents, `data:text/html;charset=utf-8,${encodeURIComponent(content)}`);
26060
26389
  });
26061
- electron.ipcMain.handle(Channels.SECURITY_PROCEED_ANYWAY, (_, tabId) => {
26390
+ electron.ipcMain.handle(Channels.SECURITY_PROCEED_ANYWAY, (event, tabId) => {
26391
+ assertTrustedIpcSender(event);
26062
26392
  assertString(tabId, "tabId");
26063
26393
  tabManager.proceedAnyway(tabId);
26064
26394
  });
26065
- electron.ipcMain.handle(Channels.SECURITY_GO_BACK_TO_SAFETY, (_, tabId) => {
26395
+ electron.ipcMain.handle(Channels.SECURITY_GO_BACK_TO_SAFETY, (event, tabId) => {
26396
+ assertTrustedIpcSender(event);
26066
26397
  assertString(tabId, "tabId");
26067
26398
  tabManager.goBackToSafety(tabId);
26068
26399
  });
@@ -26070,6 +26401,7 @@ function registerSecurityHandlers(tabManager) {
26070
26401
  const logger$5 = createLogger("CodexIPC");
26071
26402
  function registerCodexHandlers() {
26072
26403
  electron.ipcMain.handle(Channels.CODEX_START_AUTH, async (event) => {
26404
+ assertTrustedIpcSender(event);
26073
26405
  const wc = event.sender;
26074
26406
  if (!wc || wc.isDestroyed()) {
26075
26407
  return {
@@ -26100,29 +26432,61 @@ function registerCodexHandlers() {
26100
26432
  };
26101
26433
  }
26102
26434
  });
26103
- electron.ipcMain.handle(Channels.CODEX_CANCEL_AUTH, () => {
26435
+ electron.ipcMain.handle(Channels.CODEX_CANCEL_AUTH, (event) => {
26436
+ assertTrustedIpcSender(event);
26104
26437
  cancelCodexOAuth();
26105
26438
  return { ok: true };
26106
26439
  });
26107
- electron.ipcMain.handle(Channels.CODEX_DISCONNECT, () => {
26440
+ electron.ipcMain.handle(Channels.CODEX_DISCONNECT, (event) => {
26441
+ assertTrustedIpcSender(event);
26108
26442
  clearStoredCodexTokens();
26109
26443
  return { ok: true };
26110
26444
  });
26111
26445
  }
26112
26446
  const filePath = () => path$1.join(electron.app.getPath("userData"), "vessel-permissions.json");
26447
+ const ALLOWED_PERMISSION_TYPES = /* @__PURE__ */ new Set([
26448
+ "clipboard-read",
26449
+ "fullscreen",
26450
+ "geolocation",
26451
+ "media",
26452
+ "midiSysex",
26453
+ "notifications",
26454
+ "pointerLock"
26455
+ ]);
26456
+ function parseOrigin(value) {
26457
+ try {
26458
+ const origin = new URL(value).origin;
26459
+ return origin === "null" ? null : origin;
26460
+ } catch {
26461
+ return null;
26462
+ }
26463
+ }
26464
+ function isPermissionRecord(value) {
26465
+ if (!value || typeof value !== "object") return false;
26466
+ const record = value;
26467
+ return typeof record.origin === "string" && parseOrigin(record.origin) === record.origin && typeof record.permission === "string" && ALLOWED_PERMISSION_TYPES.has(record.permission) && (record.decision === "allow" || record.decision === "deny") && typeof record.updatedAt === "string";
26468
+ }
26113
26469
  let records = loadJsonFile({
26114
26470
  filePath: filePath(),
26115
26471
  fallback: [],
26116
- parse: (raw) => Array.isArray(raw) ? raw : []
26472
+ parse: (raw) => Array.isArray(raw) ? raw.filter(isPermissionRecord) : []
26117
26473
  });
26118
26474
  const persistence = createDebouncedJsonPersistence({ debounceMs: 250, filePath: filePath(), getValue: () => records, logLabel: "permissions" });
26475
+ const sessionDecisions = /* @__PURE__ */ new Map();
26119
26476
  let broadcaster = null;
26120
26477
  function key(origin, permission) {
26121
26478
  return `${origin}
26122
26479
  ${permission}`;
26123
26480
  }
26481
+ function snapshot() {
26482
+ return records.map((record) => ({ ...record }));
26483
+ }
26124
26484
  function emit() {
26125
- broadcaster?.(Channels.PERMISSIONS_GET, records);
26485
+ broadcaster?.(Channels.PERMISSIONS_GET, snapshot());
26486
+ }
26487
+ function getDecision(origin, permission) {
26488
+ const existing = records.find((r) => r.origin === origin && r.permission === permission);
26489
+ return existing?.decision ?? sessionDecisions.get(key(origin, permission)) ?? null;
26126
26490
  }
26127
26491
  function save(origin, permission, decision) {
26128
26492
  const k = key(origin, permission);
@@ -26134,15 +26498,21 @@ function save(origin, permission, decision) {
26134
26498
  emit();
26135
26499
  }
26136
26500
  function listPermissions() {
26137
- return records;
26501
+ return snapshot();
26138
26502
  }
26139
26503
  function clearPermissions() {
26140
26504
  records = [];
26505
+ sessionDecisions.clear();
26141
26506
  persistence.schedule();
26142
26507
  emit();
26143
26508
  }
26144
26509
  function clearPermissionsForOrigin(origin) {
26510
+ if (!parseOrigin(origin)) return;
26145
26511
  records = records.filter((record) => record.origin !== origin);
26512
+ for (const storedKey of sessionDecisions.keys()) {
26513
+ if (storedKey.startsWith(`${origin}
26514
+ `)) sessionDecisions.delete(storedKey);
26515
+ }
26146
26516
  persistence.schedule();
26147
26517
  emit();
26148
26518
  }
@@ -26150,28 +26520,72 @@ function setPermissionBroadcaster(fn) {
26150
26520
  broadcaster = fn;
26151
26521
  }
26152
26522
  function installPermissionHandler() {
26523
+ electron.session.defaultSession.setPermissionCheckHandler((webContents, permission, requestingOrigin) => {
26524
+ if (!ALLOWED_PERMISSION_TYPES.has(permission)) return false;
26525
+ const origin = parseOrigin(requestingOrigin || webContents.getURL());
26526
+ if (!origin) return false;
26527
+ return getDecision(origin, permission) === "allow";
26528
+ });
26153
26529
  electron.session.defaultSession.setPermissionRequestHandler((webContents, permission, callback, details) => {
26154
- const origin = new URL(details.requestingUrl || webContents.getURL()).origin;
26155
- const existing = records.find((r) => r.origin === origin && r.permission === permission);
26156
- if (existing) {
26157
- callback(existing.decision === "allow");
26530
+ if (!ALLOWED_PERMISSION_TYPES.has(permission)) {
26531
+ callback(false);
26532
+ return;
26533
+ }
26534
+ const origin = parseOrigin(details.requestingUrl || webContents.getURL());
26535
+ if (!origin) {
26536
+ callback(false);
26537
+ return;
26538
+ }
26539
+ const k = key(origin, permission);
26540
+ const decision = getDecision(origin, permission);
26541
+ if (decision) {
26542
+ callback(decision === "allow");
26158
26543
  return;
26159
26544
  }
26160
26545
  const result = electron.dialog.showMessageBoxSync({
26161
26546
  type: "question",
26162
- buttons: ["Deny", "Allow"],
26547
+ buttons: [
26548
+ "Deny Once",
26549
+ "Deny Until Quit",
26550
+ "Always Deny",
26551
+ "Allow Once",
26552
+ "Allow Until Quit",
26553
+ "Always Allow"
26554
+ ],
26163
26555
  defaultId: 0,
26164
26556
  cancelId: 0,
26165
26557
  title: "Site permission request",
26166
26558
  message: `${origin} wants to use ${permission}`,
26167
- detail: "Vessel will remember your choice. You can clear saved permissions in Settings > Privacy."
26559
+ detail: "Temporary choices are safer for camera, microphone, location, and clipboard access. Persistent choices can be cleared in Settings > Privacy."
26168
26560
  });
26169
- const decision = result === 1 ? "allow" : "deny";
26170
- save(origin, permission, decision);
26171
- callback(decision === "allow");
26561
+ if (result === 1) {
26562
+ sessionDecisions.set(k, "deny");
26563
+ callback(false);
26564
+ return;
26565
+ }
26566
+ if (result === 2) {
26567
+ save(origin, permission, "deny");
26568
+ callback(false);
26569
+ return;
26570
+ }
26571
+ if (result === 3) {
26572
+ callback(true);
26573
+ return;
26574
+ }
26575
+ if (result === 4) {
26576
+ sessionDecisions.set(k, "allow");
26577
+ callback(true);
26578
+ return;
26579
+ }
26580
+ if (result === 5) {
26581
+ save(origin, permission, "allow");
26582
+ callback(true);
26583
+ return;
26584
+ }
26585
+ callback(false);
26172
26586
  });
26173
26587
  }
26174
- const NPM_PACKAGE_URL = "https://registry.npmjs.org/@quanta-intellect%2Fvessel-browser/latest";
26588
+ const GITHUB_LATEST_RELEASE_API_URL = "https://api.github.com/repos/unmodeled-tyler/quanta-vessel-browser/releases/latest";
26175
26589
  const RELEASES_URL = "https://github.com/unmodeled-tyler/quanta-vessel-browser/releases/latest";
26176
26590
  function normalizeVersion(version) {
26177
26591
  return version.replace(/^v/i, "").split(/[.-]/).slice(0, 3).map((part) => {
@@ -26192,21 +26606,22 @@ async function checkForUpdates() {
26192
26606
  const currentVersion = electron.app.getVersion();
26193
26607
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
26194
26608
  try {
26195
- const response = await fetch(NPM_PACKAGE_URL, {
26196
- headers: { accept: "application/json", "user-agent": `Vessel/${currentVersion}` }
26609
+ const response = await fetch(GITHUB_LATEST_RELEASE_API_URL, {
26610
+ headers: { accept: "application/vnd.github+json", "user-agent": `Vessel/${currentVersion}` }
26197
26611
  });
26198
26612
  if (!response.ok) {
26199
- throw new Error(`Registry responded with ${response.status}`);
26613
+ throw new Error(`GitHub Releases responded with ${response.status}`);
26200
26614
  }
26201
26615
  const body = await response.json();
26202
- const latestVersion = typeof body.version === "string" ? body.version : null;
26203
- if (!latestVersion) throw new Error("Registry response did not include a version");
26616
+ const latestVersion = typeof body.tag_name === "string" ? body.tag_name : null;
26617
+ if (!latestVersion) throw new Error("GitHub release response did not include a tag name");
26618
+ const releaseUrl = typeof body.html_url === "string" && body.html_url.startsWith("https://github.com/") ? body.html_url : RELEASES_URL;
26204
26619
  return {
26205
26620
  currentVersion,
26206
26621
  latestVersion,
26207
26622
  updateAvailable: compareVersions(latestVersion, currentVersion) > 0,
26208
26623
  checkedAt,
26209
- releaseUrl: RELEASES_URL
26624
+ releaseUrl
26210
26625
  };
26211
26626
  } catch (error) {
26212
26627
  return {
@@ -26220,7 +26635,7 @@ async function checkForUpdates() {
26220
26635
  }
26221
26636
  }
26222
26637
  async function openUpdateDownload() {
26223
- await electron.shell.openExternal(RELEASES_URL);
26638
+ await openExternalAllowlisted(RELEASES_URL, { hosts: ["github.com"] });
26224
26639
  }
26225
26640
  let activeChatProvider = null;
26226
26641
  const logger$4 = createLogger("IPC");
@@ -26255,13 +26670,24 @@ async function togglePictureInPicture(tabManager) {
26255
26670
  }
26256
26671
  function registerIpcHandlers(windowState, runtime2) {
26257
26672
  const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
26258
- electron.ipcMain.handle(Channels.OPEN_PRIVATE_WINDOW, () => {
26673
+ registerTrustedIpcSender(chromeView.webContents);
26674
+ registerTrustedIpcSender(sidebarView.webContents);
26675
+ registerTrustedIpcSender(devtoolsPanelView.webContents);
26676
+ const requireTrusted = (event) => {
26677
+ assertTrustedIpcSender(event);
26678
+ };
26679
+ electron.ipcMain.handle(Channels.OPEN_PRIVATE_WINDOW, (event) => {
26680
+ requireTrusted(event);
26259
26681
  createPrivateWindow();
26260
26682
  });
26261
- electron.ipcMain.handle(Channels.OPEN_NEW_WINDOW, () => {
26683
+ electron.ipcMain.handle(Channels.OPEN_NEW_WINDOW, (event) => {
26684
+ requireTrusted(event);
26262
26685
  createSecondaryWindow();
26263
26686
  });
26264
- electron.ipcMain.handle(Channels.IS_PRIVATE_MODE, () => false);
26687
+ electron.ipcMain.handle(Channels.IS_PRIVATE_MODE, (event) => {
26688
+ requireTrusted(event);
26689
+ return false;
26690
+ });
26265
26691
  let sidebarResizeRecoveryTimer = null;
26266
26692
  let sidebarResizeActive = false;
26267
26693
  let runtimeUpdateTimer = null;
@@ -26342,37 +26768,45 @@ function registerIpcHandlers(windowState, runtime2) {
26342
26768
  onAIStreamIdle(() => {
26343
26769
  sendToRendererViews(Channels.AI_STREAM_IDLE);
26344
26770
  });
26345
- electron.ipcMain.handle(Channels.TAB_CREATE, (_, url) => {
26771
+ electron.ipcMain.handle(Channels.TAB_CREATE, (event, url) => {
26772
+ requireTrusted(event);
26346
26773
  const id = tabManager.createTab(url || loadSettings().defaultUrl);
26347
26774
  layoutViews(windowState);
26348
26775
  return id;
26349
26776
  });
26350
- electron.ipcMain.handle(Channels.TAB_CLOSE, (_, id) => {
26777
+ electron.ipcMain.handle(Channels.TAB_CLOSE, (event, id) => {
26778
+ requireTrusted(event);
26351
26779
  tabManager.closeTab(id);
26352
26780
  layoutViews(windowState);
26353
26781
  });
26354
- electron.ipcMain.handle(Channels.TAB_SWITCH, (_, id) => {
26782
+ electron.ipcMain.handle(Channels.TAB_SWITCH, (event, id) => {
26783
+ requireTrusted(event);
26355
26784
  tabManager.switchTab(id);
26356
26785
  layoutViews(windowState);
26357
26786
  });
26358
26787
  electron.ipcMain.handle(
26359
26788
  Channels.TAB_NAVIGATE,
26360
- (_, id, url, postBody) => {
26789
+ (event, id, url, postBody) => {
26790
+ requireTrusted(event);
26361
26791
  assertString(id, "tabId");
26362
26792
  assertString(url, "url");
26363
26793
  return tabManager.navigateTab(id, url, postBody);
26364
26794
  }
26365
26795
  );
26366
- electron.ipcMain.handle(Channels.TAB_BACK, (_, id) => {
26796
+ electron.ipcMain.handle(Channels.TAB_BACK, (event, id) => {
26797
+ requireTrusted(event);
26367
26798
  tabManager.goBack(id);
26368
26799
  });
26369
- electron.ipcMain.handle(Channels.TAB_FORWARD, (_, id) => {
26800
+ electron.ipcMain.handle(Channels.TAB_FORWARD, (event, id) => {
26801
+ requireTrusted(event);
26370
26802
  tabManager.goForward(id);
26371
26803
  });
26372
- electron.ipcMain.handle(Channels.TAB_RELOAD, (_, id) => {
26804
+ electron.ipcMain.handle(Channels.TAB_RELOAD, (event, id) => {
26805
+ requireTrusted(event);
26373
26806
  tabManager.reloadTab(id);
26374
26807
  });
26375
- electron.ipcMain.handle(Channels.TAB_TOGGLE_AD_BLOCK, (_, id) => {
26808
+ electron.ipcMain.handle(Channels.TAB_TOGGLE_AD_BLOCK, (event, id) => {
26809
+ requireTrusted(event);
26376
26810
  assertString(id, "id");
26377
26811
  const tab = tabManager.getTab(id);
26378
26812
  if (!tab) return null;
@@ -26380,87 +26814,108 @@ function registerIpcHandlers(windowState, runtime2) {
26380
26814
  tab.setAdBlockingEnabled(newState);
26381
26815
  return newState;
26382
26816
  });
26383
- electron.ipcMain.handle(Channels.TAB_ZOOM_IN, (_, id) => {
26817
+ electron.ipcMain.handle(Channels.TAB_ZOOM_IN, (event, id) => {
26818
+ requireTrusted(event);
26384
26819
  assertString(id, "id");
26385
26820
  tabManager.zoomIn(id);
26386
26821
  });
26387
- electron.ipcMain.handle(Channels.TAB_ZOOM_OUT, (_, id) => {
26822
+ electron.ipcMain.handle(Channels.TAB_ZOOM_OUT, (event, id) => {
26823
+ requireTrusted(event);
26388
26824
  assertString(id, "id");
26389
26825
  tabManager.zoomOut(id);
26390
26826
  });
26391
- electron.ipcMain.handle(Channels.TAB_ZOOM_RESET, (_, id) => {
26827
+ electron.ipcMain.handle(Channels.TAB_ZOOM_RESET, (event, id) => {
26828
+ requireTrusted(event);
26392
26829
  assertString(id, "id");
26393
26830
  tabManager.zoomReset(id);
26394
26831
  });
26395
- electron.ipcMain.handle(Channels.TAB_REOPEN_CLOSED, () => {
26832
+ electron.ipcMain.handle(Channels.TAB_REOPEN_CLOSED, (event) => {
26833
+ requireTrusted(event);
26396
26834
  const id = tabManager.reopenClosedTab();
26397
26835
  if (id) layoutViews(windowState);
26398
26836
  return id;
26399
26837
  });
26400
- electron.ipcMain.handle(Channels.TAB_DUPLICATE, (_, id) => {
26838
+ electron.ipcMain.handle(Channels.TAB_DUPLICATE, (event, id) => {
26839
+ requireTrusted(event);
26401
26840
  assertString(id, "id");
26402
26841
  const newId = tabManager.duplicateTab(id);
26403
26842
  if (newId) layoutViews(windowState);
26404
26843
  return newId;
26405
26844
  });
26406
- electron.ipcMain.handle(Channels.TAB_PIN, (_, id) => {
26845
+ electron.ipcMain.handle(Channels.TAB_PIN, (event, id) => {
26846
+ requireTrusted(event);
26407
26847
  assertString(id, "id");
26408
26848
  tabManager.pinTab(id);
26409
26849
  });
26410
- electron.ipcMain.handle(Channels.TAB_UNPIN, (_, id) => {
26850
+ electron.ipcMain.handle(Channels.TAB_UNPIN, (event, id) => {
26851
+ requireTrusted(event);
26411
26852
  assertString(id, "id");
26412
26853
  tabManager.unpinTab(id);
26413
26854
  });
26414
- electron.ipcMain.handle(Channels.TAB_GROUP_CREATE, (_, id) => {
26855
+ electron.ipcMain.handle(Channels.TAB_GROUP_CREATE, (event, id) => {
26856
+ requireTrusted(event);
26415
26857
  assertString(id, "id");
26416
26858
  return tabManager.createGroupFromTab(id);
26417
26859
  });
26418
- electron.ipcMain.handle(Channels.TAB_GROUP_ADD_TAB, (_, id, groupId) => {
26860
+ electron.ipcMain.handle(Channels.TAB_GROUP_ADD_TAB, (event, id, groupId) => {
26861
+ requireTrusted(event);
26419
26862
  assertString(id, "id");
26420
26863
  assertString(groupId, "groupId");
26421
26864
  tabManager.assignTabToGroup(id, groupId);
26422
26865
  });
26423
- electron.ipcMain.handle(Channels.TAB_GROUP_REMOVE_TAB, (_, id) => {
26866
+ electron.ipcMain.handle(Channels.TAB_GROUP_REMOVE_TAB, (event, id) => {
26867
+ requireTrusted(event);
26424
26868
  assertString(id, "id");
26425
26869
  tabManager.removeTabFromGroup(id);
26426
26870
  });
26427
- electron.ipcMain.handle(Channels.TAB_GROUP_TOGGLE_COLLAPSED, (_, groupId) => {
26871
+ electron.ipcMain.handle(Channels.TAB_GROUP_TOGGLE_COLLAPSED, (event, groupId) => {
26872
+ requireTrusted(event);
26428
26873
  assertString(groupId, "groupId");
26429
26874
  return tabManager.toggleGroupCollapsed(groupId);
26430
26875
  });
26431
26876
  electron.ipcMain.handle(
26432
26877
  Channels.TAB_GROUP_SET_COLOR,
26433
- (_, groupId, color) => {
26878
+ (event, groupId, color) => {
26879
+ requireTrusted(event);
26434
26880
  assertString(groupId, "groupId");
26435
26881
  assertString(color, "color");
26436
26882
  tabManager.setGroupColor(groupId, color);
26437
26883
  }
26438
26884
  );
26439
- electron.ipcMain.handle(Channels.TAB_TOGGLE_MUTE, (_, id) => {
26885
+ electron.ipcMain.handle(Channels.TAB_TOGGLE_MUTE, (event, id) => {
26886
+ requireTrusted(event);
26440
26887
  assertString(id, "id");
26441
26888
  return tabManager.toggleMuted(id);
26442
26889
  });
26443
- electron.ipcMain.handle(Channels.TAB_PRINT, (_, id) => {
26890
+ electron.ipcMain.handle(Channels.TAB_PRINT, (event, id) => {
26891
+ requireTrusted(event);
26444
26892
  assertString(id, "id");
26445
26893
  tabManager.printTab(id);
26446
26894
  });
26447
- electron.ipcMain.handle(Channels.TAB_PRINT_TO_PDF, (_, id) => {
26895
+ electron.ipcMain.handle(Channels.TAB_PRINT_TO_PDF, (event, id) => {
26896
+ requireTrusted(event);
26448
26897
  assertString(id, "id");
26449
26898
  return tabManager.saveTabAsPdf(id);
26450
26899
  });
26451
- electron.ipcMain.on(Channels.TAB_CONTEXT_MENU, (_event, id) => {
26900
+ electron.ipcMain.on(Channels.TAB_CONTEXT_MENU, (event, id) => {
26901
+ requireTrusted(event);
26452
26902
  assertString(id, "id");
26453
26903
  showTabContextMenu(tabManager, id, mainWindow, () => layoutViews(windowState));
26454
26904
  });
26455
- electron.ipcMain.on(Channels.TAB_GROUP_CONTEXT_MENU, (_event, groupId) => {
26905
+ electron.ipcMain.on(Channels.TAB_GROUP_CONTEXT_MENU, (event, groupId) => {
26906
+ requireTrusted(event);
26456
26907
  assertString(groupId, "groupId");
26457
26908
  showGroupContextMenu(tabManager, groupId, mainWindow);
26458
26909
  });
26459
- electron.ipcMain.handle(Channels.TAB_STATE_GET, () => ({
26460
- tabs: tabManager.getAllStates(),
26461
- activeId: tabManager.getActiveTabId() || ""
26462
- }));
26463
- electron.ipcMain.handle(Channels.AI_QUERY, async (_, query, history) => {
26910
+ electron.ipcMain.handle(Channels.TAB_STATE_GET, (event) => {
26911
+ requireTrusted(event);
26912
+ return {
26913
+ tabs: tabManager.getAllStates(),
26914
+ activeId: tabManager.getActiveTabId() || ""
26915
+ };
26916
+ });
26917
+ electron.ipcMain.handle(Channels.AI_QUERY, async (event, query, history) => {
26918
+ requireTrusted(event);
26464
26919
  const settings2 = loadSettings();
26465
26920
  const chatConfig = settings2.chatProvider;
26466
26921
  if (!chatConfig) {
@@ -26503,10 +26958,12 @@ function registerIpcHandlers(windowState, runtime2) {
26503
26958
  })();
26504
26959
  return { accepted: true };
26505
26960
  });
26506
- electron.ipcMain.handle(Channels.AI_CANCEL, () => {
26961
+ electron.ipcMain.handle(Channels.AI_CANCEL, (event) => {
26962
+ requireTrusted(event);
26507
26963
  activeChatProvider?.cancel();
26508
26964
  });
26509
- electron.ipcMain.handle(Channels.AI_FETCH_MODELS, async (_, config) => {
26965
+ electron.ipcMain.handle(Channels.AI_FETCH_MODELS, async (event, config) => {
26966
+ requireTrusted(event);
26510
26967
  try {
26511
26968
  if (!config || typeof config !== "object" || !("id" in config)) {
26512
26969
  return errorResult("Invalid provider configuration", { models: [] });
@@ -26518,31 +26975,35 @@ function registerIpcHandlers(windowState, runtime2) {
26518
26975
  return errorResult(getErrorMessage(err), { models: [] });
26519
26976
  }
26520
26977
  });
26521
- electron.ipcMain.handle(Channels.CONTENT_EXTRACT, async () => {
26978
+ electron.ipcMain.handle(Channels.CONTENT_EXTRACT, async (event) => {
26979
+ requireTrusted(event);
26522
26980
  const activeTab = tabManager.getActiveTab();
26523
26981
  if (!activeTab) return null;
26524
26982
  return extractContent(activeTab.view.webContents);
26525
26983
  });
26526
- electron.ipcMain.handle(Channels.READER_MODE_TOGGLE, async () => {
26984
+ electron.ipcMain.handle(Channels.READER_MODE_TOGGLE, async (event) => {
26985
+ requireTrusted(event);
26527
26986
  const activeTab = tabManager.getActiveTab();
26528
26987
  if (!activeTab) return;
26529
26988
  if (activeTab.state.isReaderMode) {
26530
26989
  const originalUrl = activeTab.readerOriginalUrl;
26531
26990
  activeTab.setReaderMode(false);
26532
26991
  if (originalUrl) {
26533
- activeTab.view.webContents.loadURL(originalUrl);
26992
+ void loadPermittedNavigationURL(activeTab.view.webContents, originalUrl);
26534
26993
  }
26535
26994
  } else {
26536
26995
  const originalUrl = activeTab.state.url;
26537
26996
  const content = await extractContent(activeTab.view.webContents);
26538
26997
  const html = generateReaderHTML(content);
26539
26998
  activeTab.setReaderMode(true, originalUrl);
26540
- activeTab.view.webContents.loadURL(
26999
+ void loadInternalDataURL(
27000
+ activeTab.view.webContents,
26541
27001
  `data:text/html;charset=utf-8,${encodeURIComponent(html)}`
26542
27002
  );
26543
27003
  }
26544
27004
  });
26545
- electron.ipcMain.handle(Channels.SIDEBAR_TOGGLE, () => {
27005
+ electron.ipcMain.handle(Channels.SIDEBAR_TOGGLE, (event) => {
27006
+ requireTrusted(event);
26546
27007
  windowState.uiState.sidebarOpen = !windowState.uiState.sidebarOpen;
26547
27008
  layoutViews(windowState);
26548
27009
  return {
@@ -26550,7 +27011,8 @@ function registerIpcHandlers(windowState, runtime2) {
26550
27011
  width: windowState.uiState.sidebarWidth
26551
27012
  };
26552
27013
  });
26553
- electron.ipcMain.handle(Channels.SIDEBAR_NAVIGATE, (_, tab) => {
27014
+ electron.ipcMain.handle(Channels.SIDEBAR_NAVIGATE, (event, tab) => {
27015
+ requireTrusted(event);
26554
27016
  assertString(tab, "tab");
26555
27017
  if (!windowState.uiState.sidebarOpen) {
26556
27018
  windowState.uiState.sidebarOpen = true;
@@ -26564,7 +27026,8 @@ function registerIpcHandlers(windowState, runtime2) {
26564
27026
  width: windowState.uiState.sidebarWidth
26565
27027
  };
26566
27028
  });
26567
- electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_START, () => {
27029
+ electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_START, (event) => {
27030
+ requireTrusted(event);
26568
27031
  sidebarResizeActive = true;
26569
27032
  clearSidebarResizeRecoveryTimer();
26570
27033
  const [width, height] = windowState.mainWindow.getContentSize();
@@ -26579,14 +27042,16 @@ function registerIpcHandlers(windowState, runtime2) {
26579
27042
  });
26580
27043
  scheduleSidebarResizeRecovery();
26581
27044
  });
26582
- electron.ipcMain.handle(Channels.SIDEBAR_RESIZE, (_, width) => {
27045
+ electron.ipcMain.handle(Channels.SIDEBAR_RESIZE, (event, width) => {
27046
+ requireTrusted(event);
26583
27047
  assertNumber(width, "width");
26584
27048
  const clamped = Math.max(240, Math.min(800, Math.round(width)));
26585
27049
  windowState.uiState.sidebarWidth = clamped;
26586
27050
  resizeSidebarViews(windowState);
26587
27051
  return clamped;
26588
27052
  });
26589
- electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_COMMIT, () => {
27053
+ electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_COMMIT, (event) => {
27054
+ requireTrusted(event);
26590
27055
  sidebarResizeActive = false;
26591
27056
  clearSidebarResizeRecoveryTimer();
26592
27057
  setSetting("sidebarWidth", windowState.uiState.sidebarWidth);
@@ -26594,7 +27059,8 @@ function registerIpcHandlers(windowState, runtime2) {
26594
27059
  });
26595
27060
  electron.ipcMain.on(
26596
27061
  Channels.RENDERER_VIEW_READY,
26597
- (_event, view) => {
27062
+ (event, view) => {
27063
+ requireTrusted(event);
26598
27064
  if (view !== "sidebar") return;
26599
27065
  if (!windowState.uiState.sidebarOpen) {
26600
27066
  windowState.uiState.sidebarOpen = true;
@@ -26602,12 +27068,14 @@ function registerIpcHandlers(windowState, runtime2) {
26602
27068
  }
26603
27069
  }
26604
27070
  );
26605
- electron.ipcMain.handle(Channels.FOCUS_MODE_TOGGLE, () => {
27071
+ electron.ipcMain.handle(Channels.FOCUS_MODE_TOGGLE, (event) => {
27072
+ requireTrusted(event);
26606
27073
  windowState.uiState.focusMode = !windowState.uiState.focusMode;
26607
27074
  layoutViews(windowState);
26608
27075
  return windowState.uiState.focusMode;
26609
27076
  });
26610
- electron.ipcMain.handle(Channels.SETTINGS_VISIBILITY, (_, open) => {
27077
+ electron.ipcMain.handle(Channels.SETTINGS_VISIBILITY, (event, open) => {
27078
+ requireTrusted(event);
26611
27079
  windowState.uiState.settingsOpen = open;
26612
27080
  if (open) {
26613
27081
  windowState.uiState.sidebarOpen = false;
@@ -26615,11 +27083,20 @@ function registerIpcHandlers(windowState, runtime2) {
26615
27083
  layoutViews(windowState);
26616
27084
  return windowState.uiState.settingsOpen;
26617
27085
  });
26618
- electron.ipcMain.handle(Channels.SETTINGS_GET, () => {
27086
+ electron.ipcMain.handle(Channels.SETTINGS_GET, (event) => {
27087
+ requireTrusted(event);
26619
27088
  return getRendererSettings();
26620
27089
  });
26621
- electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, () => getRuntimeHealth());
26622
- electron.ipcMain.handle(Channels.SETTINGS_SET, async (_, key2, value) => {
27090
+ electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, (event) => {
27091
+ requireTrusted(event);
27092
+ return getRuntimeHealth();
27093
+ });
27094
+ electron.ipcMain.handle(Channels.MCP_REGENERATE_TOKEN, (event) => {
27095
+ requireTrusted(event);
27096
+ return regenerateMcpAuthToken();
27097
+ });
27098
+ electron.ipcMain.handle(Channels.SETTINGS_SET, async (event, key2, value) => {
27099
+ requireTrusted(event);
26623
27100
  assertString(key2, "key");
26624
27101
  if (!SETTABLE_KEYS.has(key2)) {
26625
27102
  throw new Error(`Unknown setting key: ${key2}`);
@@ -26638,12 +27115,22 @@ function registerIpcHandlers(windowState, runtime2) {
26638
27115
  sendToRendererViews(Channels.SETTINGS_UPDATE, rendererSettings);
26639
27116
  return rendererSettings;
26640
27117
  });
26641
- electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, () => runtime2.getState());
26642
- electron.ipcMain.handle(Channels.AGENT_PAUSE, () => runtime2.pause());
26643
- electron.ipcMain.handle(Channels.AGENT_RESUME, () => runtime2.resume());
27118
+ electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, (event) => {
27119
+ requireTrusted(event);
27120
+ return runtime2.getState();
27121
+ });
27122
+ electron.ipcMain.handle(Channels.AGENT_PAUSE, (event) => {
27123
+ requireTrusted(event);
27124
+ return runtime2.pause();
27125
+ });
27126
+ electron.ipcMain.handle(Channels.AGENT_RESUME, (event) => {
27127
+ requireTrusted(event);
27128
+ return runtime2.resume();
27129
+ });
26644
27130
  electron.ipcMain.handle(
26645
27131
  Channels.AGENT_SET_APPROVAL_MODE,
26646
- (_, mode) => {
27132
+ (event, mode) => {
27133
+ requireTrusted(event);
26647
27134
  assertString(mode, "mode");
26648
27135
  if (!VALID_APPROVAL_MODES.includes(mode)) {
26649
27136
  throw new Error(`Invalid approval mode: ${mode}`);
@@ -26655,34 +27142,44 @@ function registerIpcHandlers(windowState, runtime2) {
26655
27142
  );
26656
27143
  electron.ipcMain.handle(
26657
27144
  Channels.AGENT_APPROVAL_RESOLVE,
26658
- (_, approvalId, approved) => runtime2.resolveApproval(approvalId, approved)
27145
+ (event, approvalId, approved) => {
27146
+ requireTrusted(event);
27147
+ return runtime2.resolveApproval(approvalId, approved);
27148
+ }
26659
27149
  );
26660
27150
  electron.ipcMain.handle(
26661
27151
  Channels.AGENT_CHECKPOINT_CREATE,
26662
- (_, name, note) => runtime2.createCheckpoint(name, note)
26663
- );
26664
- electron.ipcMain.handle(
26665
- Channels.AGENT_CHECKPOINT_RESTORE,
26666
- (_, checkpointId) => runtime2.restoreCheckpoint(checkpointId)
26667
- );
26668
- electron.ipcMain.handle(
26669
- Channels.AGENT_CHECKPOINT_UPDATE_NOTE,
26670
- (_, checkpointId, note) => runtime2.updateCheckpointNote(checkpointId, note || "")
26671
- );
26672
- electron.ipcMain.handle(
26673
- Channels.AGENT_UNDO_LAST_ACTION,
26674
- () => runtime2.undoLastAction()
26675
- );
26676
- electron.ipcMain.handle(
26677
- Channels.AGENT_SESSION_CAPTURE,
26678
- (_, note) => runtime2.captureSession(note)
27152
+ (event, name, note) => {
27153
+ requireTrusted(event);
27154
+ return runtime2.createCheckpoint(name, note);
27155
+ }
26679
27156
  );
27157
+ electron.ipcMain.handle(Channels.AGENT_CHECKPOINT_RESTORE, (event, checkpointId) => {
27158
+ requireTrusted(event);
27159
+ return runtime2.restoreCheckpoint(checkpointId);
27160
+ });
27161
+ electron.ipcMain.handle(Channels.AGENT_CHECKPOINT_UPDATE_NOTE, (event, checkpointId, note) => {
27162
+ requireTrusted(event);
27163
+ return runtime2.updateCheckpointNote(checkpointId, note || "");
27164
+ });
27165
+ electron.ipcMain.handle(Channels.AGENT_UNDO_LAST_ACTION, (event) => {
27166
+ requireTrusted(event);
27167
+ return runtime2.undoLastAction();
27168
+ });
27169
+ electron.ipcMain.handle(Channels.AGENT_SESSION_CAPTURE, (event, note) => {
27170
+ requireTrusted(event);
27171
+ return runtime2.captureSession(note);
27172
+ });
26680
27173
  electron.ipcMain.handle(
26681
27174
  Channels.AGENT_SESSION_RESTORE,
26682
- (_, snapshot) => runtime2.restoreSession(snapshot)
27175
+ (event, snapshot2) => {
27176
+ requireTrusted(event);
27177
+ return runtime2.restoreSession(snapshot2);
27178
+ }
26683
27179
  );
26684
27180
  registerBookmarkHandlers();
26685
- electron.ipcMain.handle(Channels.HIGHLIGHT_CAPTURE, async () => {
27181
+ electron.ipcMain.handle(Channels.HIGHLIGHT_CAPTURE, async (event) => {
27182
+ requireTrusted(event);
26686
27183
  try {
26687
27184
  const activeTab = tabManager.getActiveTab();
26688
27185
  if (!activeTab) {
@@ -26726,10 +27223,12 @@ function registerIpcHandlers(windowState, runtime2) {
26726
27223
  logger$4.warn("Failed to persist auto-highlight selection:", err);
26727
27224
  }
26728
27225
  });
26729
- electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_COUNT, () => {
27226
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_COUNT, (event) => {
27227
+ requireTrusted(event);
26730
27228
  return getActiveHighlightCountSafe();
26731
27229
  });
26732
- electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_SCROLL, (_, index) => {
27230
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_SCROLL, (event, index) => {
27231
+ requireTrusted(event);
26733
27232
  const info = getActiveTabInfo(tabManager);
26734
27233
  if (!info) return false;
26735
27234
  try {
@@ -26739,7 +27238,8 @@ function registerIpcHandlers(windowState, runtime2) {
26739
27238
  return false;
26740
27239
  }
26741
27240
  });
26742
- electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_REMOVE, async (_, index) => {
27241
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_REMOVE, async (event, index) => {
27242
+ requireTrusted(event);
26743
27243
  const info = getActiveTabInfo(tabManager);
26744
27244
  if (!info) return false;
26745
27245
  try {
@@ -26753,7 +27253,8 @@ function registerIpcHandlers(windowState, runtime2) {
26753
27253
  return false;
26754
27254
  }
26755
27255
  });
26756
- electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_CLEAR, async () => {
27256
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_CLEAR, async (event) => {
27257
+ requireTrusted(event);
26757
27258
  const info = getActiveTabInfo(tabManager);
26758
27259
  if (!info) return false;
26759
27260
  try {
@@ -26768,22 +27269,27 @@ function registerIpcHandlers(windowState, runtime2) {
26768
27269
  }
26769
27270
  });
26770
27271
  const findBridge = createFindInPageBridge(tabManager, chromeView);
26771
- electron.ipcMain.handle(Channels.FIND_IN_PAGE_START, (_, text, options) => {
27272
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_START, (event, text, options) => {
27273
+ requireTrusted(event);
26772
27274
  return findBridge.start(text, options);
26773
27275
  });
26774
- electron.ipcMain.handle(Channels.FIND_IN_PAGE_NEXT, (_, forward) => {
27276
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_NEXT, (event, forward) => {
27277
+ requireTrusted(event);
26775
27278
  return findBridge.next(forward);
26776
27279
  });
26777
- electron.ipcMain.handle(Channels.FIND_IN_PAGE_STOP, (_, action) => {
27280
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_STOP, (event, action) => {
27281
+ requireTrusted(event);
26778
27282
  findBridge.stop(action);
26779
27283
  });
26780
27284
  registerHistoryHandlers();
26781
- electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_TOGGLE, () => {
27285
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_TOGGLE, (event) => {
27286
+ requireTrusted(event);
26782
27287
  windowState.uiState.devtoolsPanelOpen = !windowState.uiState.devtoolsPanelOpen;
26783
27288
  layoutViews(windowState);
26784
27289
  return { open: windowState.uiState.devtoolsPanelOpen };
26785
27290
  });
26786
- electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_RESIZE, (_, height) => {
27291
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_RESIZE, (event, height) => {
27292
+ requireTrusted(event);
26787
27293
  const clamped = Math.max(MIN_DEVTOOLS_PANEL, Math.min(MAX_DEVTOOLS_PANEL, Math.round(height)));
26788
27294
  windowState.uiState.devtoolsPanelHeight = clamped;
26789
27295
  layoutViews(windowState);
@@ -26796,20 +27302,24 @@ function registerIpcHandlers(windowState, runtime2) {
26796
27302
  registerHumanVaultHandlers();
26797
27303
  registerWindowControlHandlers(mainWindow);
26798
27304
  registerCodexHandlers();
26799
- electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, () => {
27305
+ electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, (event) => {
27306
+ requireTrusted(event);
26800
27307
  return getInstalledKits();
26801
27308
  });
26802
- electron.ipcMain.handle(Channels.AUTOMATION_INSTALL_FROM_FILE, async () => {
27309
+ electron.ipcMain.handle(Channels.AUTOMATION_INSTALL_FROM_FILE, async (event) => {
27310
+ requireTrusted(event);
26803
27311
  return await installKitFromFile();
26804
27312
  });
26805
- electron.ipcMain.handle(Channels.AUTOMATION_UNINSTALL, (_event, id) => {
27313
+ electron.ipcMain.handle(Channels.AUTOMATION_UNINSTALL, (event, id) => {
27314
+ requireTrusted(event);
26806
27315
  assertString(id, "id");
26807
27316
  return uninstallKit(id, getScheduledKitIds());
26808
27317
  });
26809
27318
  registerScheduleHandlers(windowState, runtime2, sendToRendererViews);
26810
27319
  registerAutofillHandlers(windowState);
26811
27320
  registerPageDiffHandlers(windowState, sendToRendererViews);
26812
- electron.ipcMain.handle(Channels.CLEAR_BROWSING_DATA, async (_, options) => {
27321
+ electron.ipcMain.handle(Channels.CLEAR_BROWSING_DATA, async (event, options) => {
27322
+ requireTrusted(event);
26813
27323
  const { cache, cookies, history, localStorage: clearLs, timeRange } = options;
26814
27324
  if (cache) {
26815
27325
  await electron.session.defaultSession.clearCache();
@@ -26826,25 +27336,47 @@ function registerIpcHandlers(windowState, runtime2) {
26826
27336
  });
26827
27337
  setDownloadBroadcaster(sendToRendererViews);
26828
27338
  setPermissionBroadcaster(sendToRendererViews);
26829
- electron.ipcMain.handle(Channels.DOWNLOADS_GET, () => listDownloads());
26830
- electron.ipcMain.handle(Channels.DOWNLOADS_CLEAR, () => {
27339
+ electron.ipcMain.handle(Channels.DOWNLOADS_GET, (event) => {
27340
+ requireTrusted(event);
27341
+ return listDownloads();
27342
+ });
27343
+ electron.ipcMain.handle(Channels.DOWNLOADS_CLEAR, (event) => {
27344
+ requireTrusted(event);
26831
27345
  clearDownloads();
26832
27346
  return true;
26833
27347
  });
26834
- electron.ipcMain.handle(Channels.DOWNLOADS_OPEN, (_event, id) => openDownload(id));
26835
- electron.ipcMain.handle(Channels.DOWNLOADS_SHOW_IN_FOLDER, (_event, id) => showDownloadInFolder(id));
26836
- electron.ipcMain.handle(Channels.PERMISSIONS_GET, () => listPermissions());
26837
- electron.ipcMain.handle(Channels.PERMISSIONS_CLEAR, () => {
27348
+ electron.ipcMain.handle(Channels.DOWNLOADS_OPEN, (event, id) => {
27349
+ requireTrusted(event);
27350
+ return openDownload(id);
27351
+ });
27352
+ electron.ipcMain.handle(Channels.DOWNLOADS_SHOW_IN_FOLDER, (event, id) => {
27353
+ requireTrusted(event);
27354
+ return showDownloadInFolder(id);
27355
+ });
27356
+ electron.ipcMain.handle(Channels.PERMISSIONS_GET, (event) => {
27357
+ requireTrusted(event);
27358
+ return listPermissions();
27359
+ });
27360
+ electron.ipcMain.handle(Channels.PERMISSIONS_CLEAR, (event) => {
27361
+ requireTrusted(event);
26838
27362
  clearPermissions();
26839
27363
  return true;
26840
27364
  });
26841
- electron.ipcMain.handle(Channels.PERMISSIONS_CLEAR_ORIGIN, (_event, origin) => {
27365
+ electron.ipcMain.handle(Channels.PERMISSIONS_CLEAR_ORIGIN, (event, origin) => {
27366
+ requireTrusted(event);
26842
27367
  clearPermissionsForOrigin(origin);
26843
27368
  return true;
26844
27369
  });
26845
- electron.ipcMain.handle(Channels.UPDATES_CHECK, () => checkForUpdates());
26846
- electron.ipcMain.handle(Channels.UPDATES_OPEN_DOWNLOAD, () => openUpdateDownload());
26847
- electron.ipcMain.handle(Channels.TAB_TOGGLE_PIP, async () => {
27370
+ electron.ipcMain.handle(Channels.UPDATES_CHECK, (event) => {
27371
+ requireTrusted(event);
27372
+ return checkForUpdates();
27373
+ });
27374
+ electron.ipcMain.handle(Channels.UPDATES_OPEN_DOWNLOAD, (event) => {
27375
+ requireTrusted(event);
27376
+ return openUpdateDownload();
27377
+ });
27378
+ electron.ipcMain.handle(Channels.TAB_TOGGLE_PIP, async (event) => {
27379
+ requireTrusted(event);
26848
27380
  return togglePictureInPicture(tabManager);
26849
27381
  });
26850
27382
  }
@@ -27276,6 +27808,7 @@ class AgentRuntime {
27276
27808
  this.state = this.loadPersistedState();
27277
27809
  onMcpStatusChange(() => this.emit());
27278
27810
  }
27811
+ tabManager;
27279
27812
  state;
27280
27813
  updateListener = null;
27281
27814
  pendingResolvers = /* @__PURE__ */ new Map();
@@ -27287,11 +27820,11 @@ class AgentRuntime {
27287
27820
  }
27288
27821
  }
27289
27822
  getState() {
27290
- const snapshot = clone(this.state);
27291
- snapshot.mcpStatus = getMcpStatus();
27292
- snapshot.canUndo = this.canUndo();
27293
- snapshot.undoInfo = this.getUndoInfo();
27294
- return snapshot;
27823
+ const snapshot2 = clone(this.state);
27824
+ snapshot2.mcpStatus = getMcpStatus();
27825
+ snapshot2.canUndo = this.canUndo();
27826
+ snapshot2.undoInfo = this.getUndoInfo();
27827
+ return snapshot2;
27295
27828
  }
27296
27829
  onTabStateChanged() {
27297
27830
  this.captureSession();
@@ -27327,13 +27860,13 @@ class AgentRuntime {
27327
27860
  return this.getState();
27328
27861
  }
27329
27862
  createCheckpoint(name, note) {
27330
- const snapshot = this.captureSession(note);
27863
+ const snapshot2 = this.captureSession(note);
27331
27864
  const checkpoint = {
27332
27865
  id: crypto$2.randomUUID(),
27333
27866
  name: name?.trim() || `Checkpoint ${this.state.checkpoints.length + 1}`,
27334
27867
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
27335
27868
  note: note?.trim() || void 0,
27336
- snapshot
27869
+ snapshot: snapshot2
27337
27870
  };
27338
27871
  this.state.checkpoints = [...this.state.checkpoints, checkpoint].slice(
27339
27872
  -20
@@ -27367,26 +27900,26 @@ class AgentRuntime {
27367
27900
  return { actionName: latest.actionName, capturedAt: latest.capturedAt };
27368
27901
  }
27369
27902
  undoLastAction() {
27370
- const snapshot = this.undoSnapshots.at(-1);
27371
- if (!snapshot) return null;
27903
+ const snapshot2 = this.undoSnapshots.at(-1);
27904
+ if (!snapshot2) return null;
27372
27905
  try {
27373
- this.tabManager.restoreSession(snapshot.snapshot);
27906
+ this.tabManager.restoreSession(snapshot2.snapshot);
27374
27907
  this.undoSnapshots.pop();
27375
27908
  } catch (error) {
27376
27909
  logger$3.error("Failed to restore undo snapshot", error);
27377
27910
  return null;
27378
27911
  }
27379
- this.captureSession(`Undid ${snapshot.actionName}`);
27380
- return snapshot.actionName;
27912
+ this.captureSession(`Undid ${snapshot2.actionName}`);
27913
+ return snapshot2.actionName;
27381
27914
  }
27382
27915
  captureSession(note) {
27383
- const snapshot = this.tabManager.snapshotSession(note);
27384
- this.state.session = snapshot;
27916
+ const snapshot2 = this.tabManager.snapshotSession(note);
27917
+ this.state.session = snapshot2;
27385
27918
  this.emit();
27386
- return clone(snapshot);
27919
+ return clone(snapshot2);
27387
27920
  }
27388
- restoreSession(snapshot) {
27389
- const target = snapshot || this.state.session;
27921
+ restoreSession(snapshot2) {
27922
+ const target = snapshot2 || this.state.session;
27390
27923
  if (!target) {
27391
27924
  return this.captureSession("No saved session to restore");
27392
27925
  }
@@ -27528,6 +28061,7 @@ ${progress}
27528
28061
  args = {},
27529
28062
  tabId = null,
27530
28063
  dangerous = false,
28064
+ requiresApproval = false,
27531
28065
  undoable,
27532
28066
  executor
27533
28067
  }) {
@@ -27547,7 +28081,7 @@ ${progress}
27547
28081
  streamId: transcriptStreamId,
27548
28082
  mode: "replace"
27549
28083
  });
27550
- const approvalReason = this.getApprovalReason(dangerous);
28084
+ const approvalReason = this.getApprovalReason(dangerous, requiresApproval);
27551
28085
  if (approvalReason) {
27552
28086
  this.publishTranscript({
27553
28087
  source,
@@ -27625,8 +28159,8 @@ ${progress}
27625
28159
  capturedAt: (/* @__PURE__ */ new Date()).toISOString()
27626
28160
  };
27627
28161
  }
27628
- pushUndoSnapshot(snapshot) {
27629
- this.undoSnapshots = [...this.undoSnapshots, snapshot].slice(
28162
+ pushUndoSnapshot(snapshot2) {
28163
+ this.undoSnapshots = [...this.undoSnapshots, snapshot2].slice(
27630
28164
  -10
27631
28165
  );
27632
28166
  }
@@ -27770,10 +28304,13 @@ ${progress}
27770
28304
  )
27771
28305
  };
27772
28306
  }
27773
- getApprovalReason(dangerous) {
28307
+ getApprovalReason(dangerous, requiresApproval) {
27774
28308
  if (this.state.supervisor.paused) {
27775
28309
  return "Agent execution is paused";
27776
28310
  }
28311
+ if (requiresApproval) {
28312
+ return "Approval required: high-risk action";
28313
+ }
27777
28314
  if (this.state.supervisor.approvalMode === "manual") {
27778
28315
  return "Approval required: ask every time mode";
27779
28316
  }