diff --git a/browser/VencordNativeStub.ts b/browser/VencordNativeStub.ts index 283150d36..15c9850f0 100644 --- a/browser/VencordNativeStub.ts +++ b/browser/VencordNativeStub.ts @@ -114,4 +114,5 @@ window.VencordNative = { }, pluginHelpers: {} as any, + csp: {} as any, }; diff --git a/src/VencordNative.ts b/src/VencordNative.ts index 156acc254..56984ef35 100644 --- a/src/VencordNative.ts +++ b/src/VencordNative.ts @@ -5,6 +5,7 @@ */ import type { Settings } from "@api/Settings"; +import { CspRequestResult } from "@main/csp/manager"; import { PluginIpcMappings } from "@main/ipcPlugins"; import { IpcEvents } from "@shared/IpcEvents"; import { IpcRes } from "@utils/types"; @@ -72,5 +73,17 @@ export default { openExternal: (url: string) => invoke(IpcEvents.OPEN_EXTERNAL, url) }, + csp: { + /** + * Note: Only supports full explicit matches, not wildcards. + * + * If `*.example.com` is allowed, `isDomainAllowed("https://sub.example.com")` will return false. + */ + isDomainAllowed: (url: string, directives: string[]) => invoke(IpcEvents.CSP_IS_DOMAIN_ALLOWED, url, directives), + removeOverride: (url: string) => invoke(IpcEvents.CSP_REMOVE_OVERRIDE, url), + requestAddOverride: (url: string, directives: string[], callerName: string) => + invoke(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, url, directives, callerName), + }, + pluginHelpers: PluginHelpers }; diff --git a/src/components/VencordSettings/CloudTab.tsx b/src/components/VencordSettings/CloudTab.tsx index b45377672..f069c5616 100644 --- a/src/components/VencordSettings/CloudTab.tsx +++ b/src/components/VencordSettings/CloudTab.tsx @@ -21,7 +21,7 @@ import { Settings, useSettings } from "@api/Settings"; import { CheckedTextInput } from "@components/CheckedTextInput"; import { Grid } from "@components/Grid"; import { Link } from "@components/Link"; -import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud"; +import { authorizeCloud, checkCloudUrlCsp, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud"; import { Margins } from "@utils/margins"; import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync"; import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common"; @@ -38,6 +38,8 @@ function validateUrl(url: string) { } async function eraseAllData() { + if (!await checkCloudUrlCsp()) return; + const res = await fetch(new URL("/v1/", getCloudUrl()), { method: "DELETE", headers: { Authorization: await getCloudAuth() } diff --git a/src/components/VencordSettings/themesStyles.css b/src/components/VencordSettings/themesStyles.css index 6b8ffdb9c..b10daff31 100644 --- a/src/components/VencordSettings/themesStyles.css +++ b/src/components/VencordSettings/themesStyles.css @@ -28,11 +28,25 @@ content: "by "; } -.vc-settings-theme-link-input { - width: 100%; +.vc-settings-csp-list { + display: flex; + flex-direction: column; + gap: 8px; } -.vc-settings-theme-add-card { - padding: 1em; - margin-bottom: 16px; +.vc-settings-csp-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + + + & a { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + line-height: 1.2em; + } + + --custom-button-button-md-height: 26px; } diff --git a/src/main/csp.ts b/src/main/csp/index.ts similarity index 69% rename from src/main/csp.ts rename to src/main/csp/index.ts index 08a200d6c..ca17a9bb3 100644 --- a/src/main/csp.ts +++ b/src/main/csp/index.ts @@ -4,59 +4,63 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { NativeSettings } from "@main/settings"; import { session } from "electron"; type PolicyMap = Record; export const ConnectSrc = ["connect-src"]; -export const MediaSrc = [...ConnectSrc, "img-src", "media-src"]; +export const ImageSrc = [...ConnectSrc, "img-src"]; export const CssSrc = ["style-src", "font-src"]; -export const MediaAndCssSrc = [...MediaSrc, ...CssSrc]; -export const MediaScriptsAndCssSrc = [...MediaAndCssSrc, "script-src", "worker-src", "frame-src"]; +export const ImageAndCssSrc = [...ImageSrc, ...CssSrc]; +export const ImageScriptsAndCssSrc = [...ImageAndCssSrc, "script-src", "worker-src", "frame-src"]; // Plugins can whitelist their own domains by importing this object in their native.ts // script and just adding to it. But generally, you should just edit this file instead export const CspPolicies: PolicyMap = { - "*.github.io": MediaAndCssSrc, // GitHub pages, used by most themes - "github.com": MediaAndCssSrc, // GitHub content (stuff uploaded to markdown forms), used by most themes - "raw.githubusercontent.com": MediaAndCssSrc, // GitHub raw, used by some themes - "*.gitlab.io": MediaAndCssSrc, // GitLab pages, used by some themes - "gitlab.com": MediaAndCssSrc, // GitLab raw, used by some themes - "*.codeberg.page": MediaAndCssSrc, // Codeberg pages, used by some themes - "codeberg.org": MediaAndCssSrc, // Codeberg raw, used by some themes + "localhost": ImageAndCssSrc, + "127.0.0.1": ImageAndCssSrc, - "*.githack.com": MediaAndCssSrc, // githack (namely raw.githack.com), used by some themes - "jsdelivr.net": MediaAndCssSrc, // jsDelivr, used by very few themes + "*.github.io": ImageAndCssSrc, // GitHub pages, used by most themes + "github.com": ImageAndCssSrc, // GitHub content (stuff uploaded to markdown forms), used by most themes + "raw.githubusercontent.com": ImageAndCssSrc, // GitHub raw, used by some themes + "*.gitlab.io": ImageAndCssSrc, // GitLab pages, used by some themes + "gitlab.com": ImageAndCssSrc, // GitLab raw, used by some themes + "*.codeberg.page": ImageAndCssSrc, // Codeberg pages, used by some themes + "codeberg.org": ImageAndCssSrc, // Codeberg raw, used by some themes + + "*.githack.com": ImageAndCssSrc, // githack (namely raw.githack.com), used by some themes + "jsdelivr.net": ImageAndCssSrc, // jsDelivr, used by very few themes "fonts.googleapis.com": CssSrc, // Google Fonts, used by many themes - "i.imgur.com": MediaSrc, // Imgur, used by some themes - "i.ibb.co": MediaSrc, // ImgBB, used by some themes - "i.pinimg.com": MediaSrc, // Pinterest, used by some themes - "*.tenor.com": MediaSrc, // Tenor, used by some themes - "files.catbox.moe": MediaSrc, // Catbox, used by some themes + "i.imgur.com": ImageSrc, // Imgur, used by some themes + "i.ibb.co": ImageSrc, // ImgBB, used by some themes + "i.pinimg.com": ImageSrc, // Pinterest, used by some themes + "*.tenor.com": ImageSrc, // Tenor, used by some themes + "files.catbox.moe": ImageAndCssSrc, // Catbox, used by some themes - "cdn.discordapp.com": MediaAndCssSrc, // Discord CDN, used by Vencord and some themes to load media - "media.discordapp.net": MediaSrc, // Discord media CDN, possible alternative to Discord CDN + "cdn.discordapp.com": ImageAndCssSrc, // Discord CDN, used by Vencord and some themes to load media + "media.discordapp.net": ImageSrc, // Discord media CDN, possible alternative to Discord CDN // CDNs used for some things by Vencord. // FIXME: we really should not be using CDNs anymore - "cdnjs.cloudflare.com": MediaScriptsAndCssSrc, - "cdn.jsdelivr.net": MediaScriptsAndCssSrc, + "cdnjs.cloudflare.com": ImageScriptsAndCssSrc, + "cdn.jsdelivr.net": ImageScriptsAndCssSrc, // Function Specific "api.github.com": ConnectSrc, // used for updating Vencord itself "ws.audioscrobbler.com": ConnectSrc, // Last.fm API "translate-pa.googleapis.com": ConnectSrc, // Google Translate API - "*.vencord.dev": MediaSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev) - "manti.vendicated.dev": MediaSrc, // ReviewDB API + "*.vencord.dev": ImageSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev) + "manti.vendicated.dev": ImageSrc, // ReviewDB API "decor.fieryflames.dev": ConnectSrc, // Decor API - "ugc.decor.fieryflames.dev": MediaSrc, // Decor CDN + "ugc.decor.fieryflames.dev": ImageSrc, // Decor CDN "sponsor.ajay.app": ConnectSrc, // Dearrow API - "dearrow-thumb.ajay.app": MediaSrc, // Dearrow Thumbnail CDN - "usrbg.is-hardly.online": MediaSrc, // USRBG API - "icons.duckduckgo.com": MediaSrc, // DuckDuckGo Favicon API (Reverse Image Search) + "dearrow-thumb.ajay.app": ImageSrc, // Dearrow Thumbnail CDN + "usrbg.is-hardly.online": ImageSrc, // USRBG API + "icons.duckduckgo.com": ImageSrc, // DuckDuckGo Favicon API (Reverse Image Search) }; const findHeader = (headers: PolicyMap, headerName: Lowercase) => { @@ -107,6 +111,12 @@ const patchCsp = (headers: PolicyMap) => { pushDirective(directive, "blob:", "data:", "vencord:"); } + for (const [host, directives] of Object.entries(NativeSettings.store.customCspRules)) { + for (const directive of directives) { + pushDirective(directive, host); + } + } + for (const [host, directives] of Object.entries(CspPolicies)) { for (const directive of directives) { pushDirective(directive, host); diff --git a/src/main/csp/manager.ts b/src/main/csp/manager.ts new file mode 100644 index 000000000..b8fbbea33 --- /dev/null +++ b/src/main/csp/manager.ts @@ -0,0 +1,125 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { NativeSettings } from "@main/settings"; +import { IpcEvents } from "@shared/IpcEvents"; +import { dialog, ipcMain, IpcMainInvokeEvent } from "electron"; + +import { CspPolicies, ImageAndCssSrc } from "."; + +export type CspRequestResult = "invalid" | "cancelled" | "unchecked" | "ok" | "conflict"; + +export function registerCspIpcHandlers() { + ipcMain.handle(IpcEvents.CSP_REMOVE_OVERRIDE, removeCspRule); + ipcMain.handle(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, addCspRule); + ipcMain.handle(IpcEvents.CSP_IS_DOMAIN_ALLOWED, isDomainAllowed); +} + +function validate(url: string, directives: string[]) { + try { + const { hostname } = new URL(url); + + if (/[;'"\\]/.test(hostname)) return false; + } catch { + return false; + } + + if (directives.length === 0) return false; + if (directives.some(d => !ImageAndCssSrc.includes(d))) return false; + + return true; +} + +function getMessage(url: string, directives: string[], callerName: string) { + const domain = new URL(url).hostname; + + const message = `${callerName} wants to allow connections to ${domain}`; + + let detail = + `Unless you recognise and fully trust ${domain}, you should cancel this request!\n\n` + + `You will have to fully close and restart ${IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} for the changes to take effect.`; + + if (directives.length === 1 && directives[0] === "connect-src") { + return { message, detail }; + } + + const contentTypes = directives + .filter(type => type !== "connect-src") + .map(type => { + switch (type) { + case "img-src": + return "Images"; + case "style-src": + return "CSS & Themes"; + case "font-src": + return "Fonts"; + default: + throw new Error(`Illegal CSP directive: ${type}`); + } + }) + .sort() + .join(", "); + + detail = `The following types of content will be allowed to load from ${domain}:\n${contentTypes}\n\n${detail}`; + + return { message, detail }; +} + +async function addCspRule(_: IpcMainInvokeEvent, url: string, directives: string[], callerName: string): Promise { + if (!validate(url, directives)) { + return "invalid"; + } + + const domain = new URL(url).hostname; + + if (domain in NativeSettings.store.customCspRules) { + return "conflict"; + } + + const { checkboxChecked, response } = await dialog.showMessageBox({ + ...getMessage(url, directives, callerName), + type: callerName ? "info" : "warning", + title: "Vencord Host Permissions", + buttons: ["Cancel", "Allow"], + defaultId: 0, + cancelId: 0, + checkboxLabel: `I fully trust ${domain} and understand the risks of allowing connections to it.`, + checkboxChecked: false, + }); + + if (response !== 1) { + return "cancelled"; + } + + if (!checkboxChecked) { + return "unchecked"; + } + + NativeSettings.store.customCspRules[domain] = directives; + return "ok"; +} + +function removeCspRule(_: IpcMainInvokeEvent, domain: string) { + if (domain in NativeSettings.store.customCspRules) { + delete NativeSettings.store.customCspRules[domain]; + return true; + } + + return false; +} + +function isDomainAllowed(_: IpcMainInvokeEvent, url: string, directives: string[]) { + try { + const domain = new URL(url).hostname; + + const ruleForDomain = CspPolicies[domain] ?? NativeSettings.store.customCspRules[domain]; + if (!ruleForDomain) return false; + + return directives.every(d => ruleForDomain.includes(d)); + } catch (e) { + return false; + } +} diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index f7034a681..aa7212a63 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -28,11 +28,14 @@ import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs"; import { open, readdir, readFile } from "fs/promises"; import { join, normalize } from "path"; +import { registerCspIpcHandlers } from "./csp/manager"; import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants"; import { makeLinksOpenExternally } from "./utils/externalLinks"; mkdirSync(THEMES_DIR, { recursive: true }); +registerCspIpcHandlers(); + export function ensureSafePath(basePath: string, path: string) { const normalizedBasePath = normalize(basePath + "/"); const newPath = join(basePath, path); diff --git a/src/main/settings.ts b/src/main/settings.ts index 3d367a945..ed2f48500 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -49,16 +49,18 @@ export interface NativeSettings { [setting: string]: any; }; }; + customCspRules: Record; } const DefaultNativeSettings: NativeSettings = { - plugins: {} + plugins: {}, + customCspRules: {} }; const nativeSettings = readSettings("native", NATIVE_SETTINGS_FILE); mergeDefaults(nativeSettings, DefaultNativeSettings); -export const NativeSettings = new SettingsStore(nativeSettings); +export const NativeSettings = new SettingsStore(nativeSettings as NativeSettings); NativeSettings.addGlobalChangeListener(() => { try { diff --git a/src/plugins/banger/index.ts b/src/plugins/banger/index.ts deleted file mode 100644 index eed0e1b44..000000000 --- a/src/plugins/banger/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -const settings = definePluginSettings({ - source: { - description: "Source to replace ban GIF with (Video or Gif)", - type: OptionType.STRING, - default: "https://i.imgur.com/wp5q52C.mp4", - restartNeeded: true, - } -}); - -export default definePlugin({ - name: "BANger", - description: "Replaces the GIF in the ban dialogue with a custom one.", - authors: [Devs.Xinto, Devs.Glitch], - settings, - patches: [ - { - find: "#{intl::jeKpoq::raw}", // BAN_CONFIRM_TITLE - replacement: { - match: /src:\i\("?\d+"?\)/g, - replace: "src:$self.source" - } - } - ], - get source() { - return settings.store.source; - } -}); diff --git a/src/plugins/experiments/index.tsx b/src/plugins/experiments/index.tsx index be18acfa2..724a3ed16 100644 --- a/src/plugins/experiments/index.tsx +++ b/src/plugins/experiments/index.tsx @@ -69,7 +69,7 @@ export default definePlugin({ }, // Change top right chat toolbar button from the help one to the dev one { - find: ".GLOBAL_DISCOVERY)?", + find: '"M9 3v18"', replacement: { match: /hasBugReporterAccess:(\i)/, replace: "_hasBugReporterAccess:$1=true" diff --git a/src/plugins/serverInfo/GuildInfoModal.tsx b/src/plugins/serverInfo/GuildInfoModal.tsx index b3bcaf55d..3acb96b88 100644 --- a/src/plugins/serverInfo/GuildInfoModal.tsx +++ b/src/plugins/serverInfo/GuildInfoModal.tsx @@ -233,7 +233,7 @@ function ServerInfoTab({ guild }: GuildProps) { "Vanity Link": guild.vanityURLCode ? ({`discord.gg/${guild.vanityURLCode}`}) : "-", // Making the anchor href valid would cause Discord to reload "Preferred Locale": guild.preferredLocale || "-", "Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?", - "Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`, + "Server Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`, "Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category "Roles": Object.keys(GuildStore.getRoles(guild.id)).length - 1, // - @everyone }; diff --git a/src/plugins/translate/styles.css b/src/plugins/translate/styles.css index c07c9e362..33a1fc851 100644 --- a/src/plugins/translate/styles.css +++ b/src/plugins/translate/styles.css @@ -15,6 +15,8 @@ margin-top: 0.5em; font-style: italic; font-weight: 400; + line-height: 1.2rem; + white-space: break-spaces; } .vc-trans-accessory-icon { diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index 2027df9cf..914a0a9f1 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -42,4 +42,8 @@ export const enum IpcEvents { OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect", VOICE_MESSAGES_READ_RECORDING = "VencordVMReadRecording", + + CSP_IS_DOMAIN_ALLOWED = "VencordCspIsDomainAllowed", + CSP_REMOVE_OVERRIDE = "VencordCspRemoveOverride", + CSP_REQUEST_ADD_OVERRIDE = "VencordCspRequestAddOverride", } diff --git a/src/utils/cloud.tsx b/src/utils/cloud.tsx index 5d1aa1bff..3b482d4d8 100644 --- a/src/utils/cloud.tsx +++ b/src/utils/cloud.tsx @@ -19,23 +19,40 @@ import * as DataStore from "@api/DataStore"; import { showNotification } from "@api/Notifications"; import { Settings } from "@api/Settings"; -import { OAuth2AuthorizeModal, UserStore } from "@webpack/common"; +import { Alerts, OAuth2AuthorizeModal, UserStore } from "@webpack/common"; import { Logger } from "./Logger"; import { openModal } from "./modal"; +import { relaunch } from "./native"; export const cloudLogger = new Logger("Cloud", "#39b7e0"); -export const cloudUrl = () => { - if (Settings.cloud.url.includes("https://equicord.thororen.com") || Settings.cloud.url.includes("https://cloud.equicord.fyi")) { - Settings.cloud.url = "https://cloud.equicord.org"; - Settings.cloud.authenticated = false; - deauthorizeCloud(); - } - return Settings.cloud.url; -}; -export const getCloudUrl = () => new URL(cloudUrl()); -const cloudUrlOrigin = () => getCloudUrl().origin; +export const getCloudUrl = () => new URL(Settings.cloud.url); +const getCloudUrlOrigin = () => getCloudUrl().origin; + +export async function checkCloudUrlCsp() { + if (IS_WEB) return true; + + const { host } = getCloudUrl(); + if (host === "api.vencord.dev") return true; + + if (await VencordNative.csp.isDomainAllowed(Settings.cloud.url, ["connect-src"])) { + return true; + } + + const res = await VencordNative.csp.requestAddOverride(Settings.cloud.url, ["connect-src"], "Cloud Sync"); + if (res === "ok") { + Alerts.show({ + title: "Cloud Integration enabled", + body: `${host} has been added to the whitelist. Please restart the app for the changes to take effect.`, + confirmText: "Restart now", + cancelText: "Later!", + onConfirm: relaunch + }); + } + return false; +} + const getUserId = () => { const id = UserStore.getCurrentUser()?.id; if (!id) throw new Error("User not yet logged in"); @@ -45,7 +62,7 @@ const getUserId = () => { export async function getAuthorization() { const secrets = await DataStore.get>("Vencord_cloudSecret") ?? {}; - const origin = cloudUrlOrigin(); + const origin = getCloudUrlOrigin(); // we need to migrate from the old format here if (secrets[origin]) { @@ -67,7 +84,7 @@ export async function getAuthorization() { async function setAuthorization(secret: string) { await DataStore.update>("Vencord_cloudSecret", secrets => { secrets ??= {}; - secrets[`${cloudUrlOrigin()}:${getUserId()}`] = secret; + secrets[`${getCloudUrlOrigin()}:${getUserId()}`] = secret; return secrets; }); } @@ -75,7 +92,7 @@ async function setAuthorization(secret: string) { export async function deauthorizeCloud() { await DataStore.update>("Vencord_cloudSecret", secrets => { secrets ??= {}; - delete secrets[`${cloudUrlOrigin()}:${getUserId()}`]; + delete secrets[`${getCloudUrlOrigin()}:${getUserId()}`]; return secrets; }); } @@ -86,6 +103,8 @@ export async function authorizeCloud() { return; } + if (!await checkCloudUrlCsp()) return; + try { const oauthConfiguration = await fetch(new URL("/v1/oauth/settings", getCloudUrl())); var { clientId, redirectUri } = await oauthConfiguration.json(); diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts index 19900d833..f21fe050e 100644 --- a/src/utils/settingsSync.ts +++ b/src/utils/settingsSync.ts @@ -22,7 +22,7 @@ import { PlainSettings, Settings } from "@api/Settings"; import { moment, SettingsRouter, Toasts } from "@webpack/common"; import { deflateSync, inflateSync } from "fflate"; -import { getCloudAuth, getCloudUrl } from "./cloud"; +import { checkCloudUrlCsp, getCloudAuth, getCloudUrl } from "./cloud"; import { Logger } from "./Logger"; import { relaunch } from "./native"; import { chooseFile, saveFile } from "./web"; @@ -118,6 +118,8 @@ const cloudSettingsLogger = new Logger("Cloud:Settings", "#39b7e0"); export async function putCloudSettings(manual?: boolean) { const settings = await exportSettings({ minify: true }); + if (!await checkCloudUrlCsp()) return; + try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "PUT", @@ -162,6 +164,8 @@ export async function putCloudSettings(manual?: boolean) { } export async function getCloudSettings(shouldNotify = true, force = false) { + if (!await checkCloudUrlCsp()) return; + try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "GET", @@ -264,6 +268,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) { } export async function deleteCloudSettings() { + if (!await checkCloudUrlCsp()) return; + try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "DELETE",