diff --git a/src/client/BrowserView.tsx b/src/client/BrowserView.tsx index 1fda9b2..92cd102 100644 --- a/src/client/BrowserView.tsx +++ b/src/client/BrowserView.tsx @@ -26,7 +26,7 @@ import { } from '@deepseek-ai/dsh-client-ui-primitives' import { VscLinkExternal } from 'react-icons/vsc' import { api } from './api.ts' -import { embeddabilityOf, normalizeBrowserUrl } from './browser.ts' +import { embeddabilityOf, isAllowedLoopbackUrl, normalizeBrowserUrl } from './browser.ts' import { patchTab } from './state.ts' import { SandboxStatusBar } from './SandboxStatusBar.tsx' import { t } from './locales.ts' @@ -43,6 +43,26 @@ import css from './sidebar.module.css' export const BROWSER_IFRAME_SANDBOX = 'allow-scripts allow-forms allow-popups allow-downloads allow-modals allow-popups-to-escape-sandbox' +/** allow-same-origin appended for explicitly allowlisted local addresses. */ +const BROWSER_IFRAME_SANDBOX_SAME_ORIGIN = + `${BROWSER_IFRAME_SANDBOX} allow-same-origin` + +/** + * The sandbox tokens for one URL: allowlisted loopback addresses (local dev + * servers the user explicitly trusts) additionally get `allow-same-origin` + * so Vite/module/HMR pipelines that need a real origin work; every other + * site keeps the opaque-origin sandbox. `allow-same-origin` does NOT give + * the page access to the GUI — it stays cross-origin to it and to every + * other site — but it does give it its OWN origin privileges (localStorage, + * fetch without CORS), so it is only granted for the explicit allowlist. + */ +export function iframeSandboxFor(url: string | undefined, allowedLoopback: string): string | undefined { + if (url === undefined) return undefined + return isAllowedLoopbackUrl(url, allowedLoopback) + ? BROWSER_IFRAME_SANDBOX_SAME_ORIGIN + : BROWSER_IFRAME_SANDBOX +} + export function BrowserView(props: TabComponentProps) { const { store, tab } = props // The current address (initialized from the persisted tab.path so a @@ -88,7 +108,7 @@ export function BrowserView(props: TabComponentProps) { } const navigateTo = (raw: string): void => { - const result = normalizeBrowserUrl(raw, window.location.origin) + const result = normalizeBrowserUrl(raw, window.location.origin, store.getPrefs().browserAllowedLoopback) if (result.kind === 'ok') { const next = result.url setUrl(next) @@ -210,7 +230,7 @@ export function BrowserView(props: TabComponentProps) { key={`${reloadKey}:${noSandbox ? 'ns' : 'sb'}`} className={css.browserFrame} src={url} - sandbox={noSandbox ? undefined : BROWSER_IFRAME_SANDBOX} + sandbox={noSandbox ? undefined : iframeSandboxFor(url, store.getPrefs().browserAllowedLoopback)} referrerPolicy="no-referrer" allow="" title={url} diff --git a/src/client/browser.ts b/src/client/browser.ts index 4d47da9..3aab45e 100644 --- a/src/client/browser.ts +++ b/src/client/browser.ts @@ -68,6 +68,11 @@ export function isLoopbackHostname(hostname: string): boolean { * @param selfOrigin - the GUI's own origin (window.location.origin). The GUI * itself may be browsed in the sidebar (the sandbox keeps it opaque), so it * is let through BEFORE the loopback check — its host is normally loopback. + * @param allowedLoopback - comma-separated loopback allowlist from the side + * card prefs (`browserAllowedLoopback`): bare hosts (`localhost`, + * `127.0.0.1`) allow every port, `host:port` entries allow exactly that + * authority. Entries are matched case-insensitively; portless entries match + * the host on any port. Empty allowlist keeps the default loopback block. */ /** Schemes that must never reach the iframe, even without `//` (javascript:, * data:, file:, ...). Host:port lookalikes (example.com:8080) are NOT here — @@ -78,7 +83,41 @@ const FORBIDDEN_SCHEMES = new Set([ 'chrome', 'chrome-extension', 'moz-extension', 'edge', 'opera', 'resource', 'view-source', ]) -export function normalizeBrowserUrl(input: string, selfOrigin: string): BrowserNavigateResult { +/** Parse the loopback allowlist into a matcher predicate over host:port. */ +export function parseLoopbackAllowlist(allowlist: string): (host: string, port: string) => boolean { + const entries = allowlist.split(',').map(entry => entry.trim().toLowerCase()).filter(entry => entry !== '') + const exact = new Set(entries) + const hosts = new Set() + for (const entry of entries) { + if (!entry.includes(':')) hosts.add(entry.replace(/^\[|\]$/g, '')) + } + return (host, port) => { + const key = `${host}:${port}` + if (exact.has(key) || exact.has(host)) return true + return port !== '' && hosts.has(host) + } +} + +/** + * Whether a loopback URL is explicitly allowlisted by the side card prefs + * (`browserAllowedLoopback`). Only allowlisted local addresses may run with + * `allow-same-origin` in the sidebar iframe — needed for local dev servers + * (Vite etc.) whose module/HMR/fetch pipeline requires a real origin, while + * the page stays cross-origin to the GUI and to every other site. + */ +export function isAllowedLoopbackUrl(url: string, allowlist: string): boolean { + if (allowlist.trim() === '') return false + let parsed: URL + try { + parsed = new URL(url) + } catch { + return false + } + if (!isLoopbackHostname(parsed.hostname)) return false + return parseLoopbackAllowlist(allowlist)(parsed.hostname, parsed.port) +} + +export function normalizeBrowserUrl(input: string, selfOrigin: string, allowedLoopback = ''): BrowserNavigateResult { const trimmed = input.trim() if (trimmed === '') return { kind: 'invalid' } // Distinguish an explicit scheme from a bare host:port. "example.com:8080" @@ -114,6 +153,15 @@ export function normalizeBrowserUrl(input: string, selfOrigin: string): BrowserN } catch { // Unparsable selfOrigin (never in practice): fall through to the loopback gate. } - if (isLoopbackHostname(url.hostname)) return { kind: 'blocked', reason: 'loopback' } + if (isLoopbackHostname(url.hostname)) { + // An explicit user allowlist (browserAllowedLoopback) can lift the + // loopback block for trusted local dev servers. The sandbox still + // renders them in an opaque origin — no GUI access, exactly like any + // other browsed site. + if (allowedLoopback.trim() !== '' && parseLoopbackAllowlist(allowedLoopback)(url.hostname, url.port)) { + return { kind: 'ok', url: url.href } + } + return { kind: 'blocked', reason: 'loopback' } + } return { kind: 'ok', url: url.href } } diff --git a/src/client/builtins/tabs.tsx b/src/client/builtins/tabs.tsx index f66ea1d..ee644ac 100644 --- a/src/client/builtins/tabs.tsx +++ b/src/client/builtins/tabs.tsx @@ -309,6 +309,12 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read key: 'browserInterceptHttps', title: () => t('settingsBrowserHttpsTitle'), desc: () => t('settingsBrowserHttpsDesc'), + }, { + key: 'browserAllowedLoopback', + type: 'text', + title: () => t('settingsBrowserLoopbackTitle'), + desc: () => t('settingsBrowserLoopbackDesc'), + placeholder: t('settingsBrowserLoopbackPlaceholder'), }], }, createTab: (state) => ({ diff --git a/src/client/locales-ar.ts b/src/client/locales-ar.ts index afa14fa..9c8fb10 100644 --- a/src/client/locales-ar.ts +++ b/src/client/locales-ar.ts @@ -261,6 +261,9 @@ export const ar: Record = { settingsBrowserHttpDesc: 'عند التفعيل، النقر على رابط HTTP خارجي في المحادثة أو الواجهة يفتح الشريط الجانبي (صفحات الإضافات المعرّفة عن urlTarget تفوز)؛ Ctrl/Cmd+نقر يتجاوز دائماً', settingsBrowserHttpsTitle: 'فتح صفحات HTTPS في الشريط الجانبي', settingsBrowserHttpsDesc: 'عند التفعيل، النقر على رابط HTTPS خارجي في المحادثة أو الواجهة يفتح الشريط الجانبي. معطّل افتراضياً: معظم مواقع HTTPS ترفض التضمين، لذا فالمتصفح الافتراضي للنظام هو الخيار الأنسب', + settingsBrowserLoopbackTitle: 'العناوين المحلية المسموح بها', + settingsBrowserLoopbackDesc: 'قائمة بيضاء مفصولة بفواصل لعناوين الاسترجاع المحلي (مثل localhost:5174 أو 127.0.0.1:8080) يمكن للمتصفح الجانبي زيارتها؛ فارغة افتراضيًا تمنع جميع العناوين المحلية. تظل صندوق الحماية سارية — لا يمكن للصفحات قراءة بيانات الواجهة', + settingsBrowserLoopbackPlaceholder: 'مثال localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'فتح في المتصفح', browserEmbedBlocked: 'رفض {host} التضمين', browserEmbedBlockedDesc: 'يحظر الموقع عرضه داخل صفحات أخرى (X-Frame-Options / frame-ancestors)، لذا لا يمكن تحميله في الشريط الجانبي. افتحه مباشرة في متصفحك بدلاً من ذلك.', diff --git a/src/client/locales-de.ts b/src/client/locales-de.ts index c533193..0ca7737 100644 --- a/src/client/locales-de.ts +++ b/src/client/locales-de.ts @@ -246,6 +246,9 @@ export const de: Record = { settingsBrowserHttpDesc: 'Wenn aktiviert, öffnet ein Klick auf einen HTTP-Externlink im Chat oder in der Oberfläche die Seitenleiste (Plugin-Seiten mit urlTarget gewinnen); Ctrl/Cmd-Klick umgeht dies immer', settingsBrowserHttpsTitle: 'HTTPS-Seiten in der Seitenleiste öffnen', settingsBrowserHttpsDesc: 'Wenn aktiviert, öffnet ein Klick auf einen HTTPS-Externlink im Chat oder in der Oberfläche die Seitenleiste. Standardmäßig deaktiviert: Die meisten HTTPS-Sites verweigern die Einbettung, der Systembrowser ist der reibungslosere Standard', + settingsBrowserLoopbackTitle: 'Erlaubte lokale Adressen', + settingsBrowserLoopbackDesc: 'Kommagetrennte Allowlist von Loopback-Adressen (z. B. localhost:5174 oder 127.0.0.1:8080), die der Seitenleisten-Browser besuchen darf; leer blockiert standardmäßig alle lokalen Adressen. Die Sandbox bleibt aktiv — Seiten können keine GUI-Daten lesen', + settingsBrowserLoopbackPlaceholder: 'z. B. localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'Im Browser öffnen', browserEmbedBlocked: '{host} hat die Einbettung verweigert', browserEmbedBlockedDesc: 'Die Site verbietet die Anzeige in anderen Seiten (X-Frame-Options / frame-ancestors) und kann daher nicht in der Seitenleiste geladen werden. Öffnen Sie sie stattdessen direkt im Browser.', diff --git a/src/client/locales-fr.ts b/src/client/locales-fr.ts index 7c708dd..d7e2f8f 100644 --- a/src/client/locales-fr.ts +++ b/src/client/locales-fr.ts @@ -253,6 +253,9 @@ export const fr: Record = { settingsBrowserHttpDesc: 'Une fois activé, cliquer sur un lien externe HTTP du chat ou de l’interface l’ouvre dans la barre latérale (les pages de plugin déclarant urlTarget sont prioritaires) ; Ctrl/Cmd+clic autorise temporairement', settingsBrowserHttpsTitle: 'Ouvrir les pages HTTPS de côté', settingsBrowserHttpsDesc: 'Une fois activé, cliquer sur un lien externe HTTPS du chat ou de l’interface l’ouvre dans la barre latérale. Désactivé par défaut : la plupart des sites HTTPS refusent l’encapsulation, le navigateur système est plus fluide', + settingsBrowserLoopbackTitle: 'Adresses locales autorisées', + settingsBrowserLoopbackDesc: 'Liste blanche séparée par des virgules d'adresses de bouclage (ex. localhost:5174 ou 127.0.0.1:8080) que le navigateur de la barre latérale peut visiter ; vide bloque toutes les adresses locales par défaut. Le bac à sable s'applique toujours — les pages ne peuvent pas lire les données de l'interface', + settingsBrowserLoopbackPlaceholder: 'ex. localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'Ouvrir dans le navigateur', browserEmbedBlocked: '{host} a refusé la demande d’encapsulation', browserEmbedBlockedDesc: 'Ce site interdit son affichage dans d’autres pages (X-Frame-Options / frame-ancestors) et ne peut donc pas être chargé dans la barre latérale. Vous pouvez l’ouvrir directement dans le navigateur', diff --git a/src/client/locales-hi.ts b/src/client/locales-hi.ts index 65728fb..c932216 100644 --- a/src/client/locales-hi.ts +++ b/src/client/locales-hi.ts @@ -260,6 +260,9 @@ export const hi: Record = { settingsBrowserHttpDesc: 'चालू होने पर, चैट या GUI में HTTP बाहरी लिंक क्लिक करने पर साइडबार खुलता है (urlTarget घोषित प्लगइन पृष्ठ जीतते हैं); Ctrl/Cmd+क्लिक हमेशा बायपास करता है', settingsBrowserHttpsTitle: 'HTTPS पृष्ठ साइडबार में खोलें', settingsBrowserHttpsDesc: 'चालू होने पर, चैट या GUI में HTTPS बाहरी लिंक क्लिक करने पर साइडबार खुलता है। डिफ़ॉल्ट रूप से बंद: अधिकांश HTTPS साइटें एम्बेड होने से इनकार करती हैं, सिस्टम ब्राउज़र चिकना डिफ़ॉल्ट है', + settingsBrowserLoopbackTitle: 'अनुमत स्थानीय पते', + settingsBrowserLoopbackDesc: 'लूपबैक पतों की कॉमा-सेपरेटेड अनुमति सूची (जैसे localhost:5174 या 127.0.0.1:8080) जिन्हें साइडबार ब्राउज़र देख सकता है; खाली डिफ़ॉल्ट रूप से सभी स्थानीय पते ब्लॉक करता है। सैंडबॉक्स लागू रहता है — पेज GUI डेटा नहीं पढ़ सकते', + settingsBrowserLoopbackPlaceholder: 'जैसे localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'ब्राउज़र में खोलें', browserEmbedBlocked: '{host} एम्बेड होने से इनकार', browserEmbedBlockedDesc: 'साइट अन्य पृष्ठों के अंदर प्रदर्शित होने से मना करती है (X-Frame-Options / frame-ancestors), इसलिए साइडबार में लोड नहीं हो सकती। इसे अपने ब्राउज़र में सीधे खोलें।', diff --git a/src/client/locales-id.ts b/src/client/locales-id.ts index e142582..608d734 100644 --- a/src/client/locales-id.ts +++ b/src/client/locales-id.ts @@ -258,6 +258,9 @@ export const id: Record = { settingsBrowserHttpDesc: 'Saat aktif, mengeklik tautan eksternal HTTP di obrolan atau GUI membuka sidebar (halaman plugin yang mendeklarasikan urlTarget menang); Ctrl/Cmd+klik selalu menembus', settingsBrowserHttpsTitle: 'Buka halaman HTTPS di sidebar', settingsBrowserHttpsDesc: 'Saat aktif, mengeklik tautan eksternal HTTPS di obrolan atau GUI membuka sidebar. Nonaktif secara default: kebanyakan situs HTTPS menolak untuk disematkan, jadi browser sistem adalah default yang lebih mulus', + settingsBrowserLoopbackTitle: 'Alamat lokal yang diizinkan', + settingsBrowserLoopbackDesc: 'Daftar putih alamat loopback yang dipisahkan koma (mis. localhost:5174 atau 127.0.0.1:8080) yang dapat dikunjungi browser sidebar; kosong memblokir semua alamat lokal secara default. Sandbox tetap berlaku — halaman tidak dapat membaca data GUI', + settingsBrowserLoopbackPlaceholder: 'mis. localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'Buka di browser', browserEmbedBlocked: '{host} menolak untuk disematkan', browserEmbedBlockedDesc: 'Situs melarang ditampilkan di dalam halaman lain (X-Frame-Options / frame-ancestors), sehingga tidak dapat dimuat di sidebar. Buka langsung di browser Anda.', diff --git a/src/client/locales-it.ts b/src/client/locales-it.ts index cd63c18..ad27a4a 100644 --- a/src/client/locales-it.ts +++ b/src/client/locales-it.ts @@ -251,6 +251,9 @@ export const it: Record = { settingsBrowserHttpDesc: 'Se attivato, cliccando un collegamento HTTP esterno nella chat o nell’interfaccia si apre la barra laterale (le pagine dei plugin che dichiarano urlTarget vincono); Ctrl/Cmd+clic lo aggira sempre', settingsBrowserHttpsTitle: 'Apri le pagine HTTPS nella barra laterale', settingsBrowserHttpsDesc: 'Se attivato, cliccando un collegamento HTTPS esterno nella chat o nell’interfaccia si apre la barra laterale. Disattivato per impostazione predefinita: la maggior parte dei siti HTTPS rifiuta di essere incorporata, per cui il browser di sistema è l’impostazione più fluida', + settingsBrowserLoopbackTitle: 'Indirizzi locali consentiti', + settingsBrowserLoopbackDesc: 'Allowlist separata da virgole di indirizzi di loopback (es. localhost:5174 o 127.0.0.1:8080) che il browser della barra laterale può visitare; vuoto blocca tutti gli indirizzi locali per impostazione predefinita. La sandbox si applica ancora — le pagine non possono leggere i dati della GUI', + settingsBrowserLoopbackPlaceholder: 'es. localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'Apri nel browser', browserEmbedBlocked: '{host} ha rifiutato di essere incorporato', browserEmbedBlockedDesc: 'Il sito vieta di essere mostrato all’interno di altre pagine (X-Frame-Options / frame-ancestors), per cui non può essere caricato nella barra laterale. Lo apra direttamente nel browser.', diff --git a/src/client/locales-ja.ts b/src/client/locales-ja.ts index 6728677..ac2db5a 100644 --- a/src/client/locales-ja.ts +++ b/src/client/locales-ja.ts @@ -260,6 +260,9 @@ export const ja: Record = { settingsBrowserHttpDesc: 'オンにすると、チャットや GUI の HTTP 外部リンククリック時にサイドバーで開く(urlTarget を宣言したプラグインページが優先)。Ctrl/Cmd+クリックで一時的にバイパス', settingsBrowserHttpsTitle: 'HTTPS ページをサイドバーで開く', settingsBrowserHttpsDesc: 'オンにすると、チャットや GUI の HTTPS 外部リンククリック時にサイドバーで開く。デフォルトオフ:大半の HTTPS サイトは埋め込みを拒否するため、システムブラウザーの方がスムーズ', + settingsBrowserLoopbackTitle: '許可されたローカルアドレス', + settingsBrowserLoopbackDesc: 'サイドバーブラウザがアクセスできるループバックアドレスのカンマ区切り許可リスト(例:localhost:5174 や 127.0.0.1:8080)。空の場合、デフォルトですべてのローカルアドレスをブロックします。サンドボックスは引き続き適用され、ページは GUI データを読み取れません', + settingsBrowserLoopbackPlaceholder: '例 localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'ブラウザーで開く', browserEmbedBlocked: '{host} は埋め込みを拒否しました', browserEmbedBlockedDesc: 'このサイトは X-Frame-Options / frame-ancestors で他ページ内での表示を禁止しているため、サイドバー内で読み込めません。ブラウザーで直接開いてください', diff --git a/src/client/locales-ko.ts b/src/client/locales-ko.ts index e1f4eca..752586a 100644 --- a/src/client/locales-ko.ts +++ b/src/client/locales-ko.ts @@ -252,6 +252,9 @@ export const ko: Record = { settingsBrowserHttpDesc: '켜면 채팅이나 인터페이스에서 HTTP 외부 링크를 클릭할 때 사이드바에서 엽니다(urlTarget을 선언한 플러그인 페이지가 우선). Ctrl/Cmd 클릭으로 임시 허용할 수 있습니다', settingsBrowserHttpsTitle: 'HTTPS 페이지를 사이드바에서 열기', settingsBrowserHttpsDesc: '켜면 채팅이나 인터페이스에서 HTTPS 외부 링크를 클릭할 때 사이드바에서 엽니다. 기본 꺼짐: 대부분의 HTTPS 사이트가 임베드를 거부하므로 시스템 브라우저가 더 원활합니다', + settingsBrowserLoopbackTitle: '허용된 로컬 주소', + settingsBrowserLoopbackDesc: '사이드바 브라우저가 방문할 수 있는 루프백 주소의 쉼표로 구분된 허용 목록(예: localhost:5174 또는 127.0.0.1:8080). 비어 있으면 기본적으로 모든 로컬 주소를 차단합니다. 샌드박스는 계속 적용되며, 페이지는 GUI 데이터를 읽을 수 없습니다', + settingsBrowserLoopbackPlaceholder: '예: localhost:5174, 127.0.0.1:8080', browserOpenExternal: '브라우저에서 열기', browserEmbedBlocked: '{host} 가(이) 임베드 요청을 거부했습니다', browserEmbedBlockedDesc: '이 사이트는 X-Frame-Options / frame-ancestors로 다른 페이지에 표시되는 것을 금지하여 사이드바에서 로드할 수 없습니다. 브라우저에서 직접 열 수 있습니다', diff --git a/src/client/locales-nl.ts b/src/client/locales-nl.ts index bc357ed..123f2ae 100644 --- a/src/client/locales-nl.ts +++ b/src/client/locales-nl.ts @@ -258,6 +258,9 @@ export const nl: Record = { settingsBrowserHttpDesc: 'Indien aan, opent een klik op een externe HTTP-link in de chat of GUI de zijbalk (plugin-pagina\'s die urlTarget declareren winnen); Ctrl/Cmd+klik omzeilt altijd', settingsBrowserHttpsTitle: 'HTTPS-pagina\'s in de zijbalk openen', settingsBrowserHttpsDesc: 'Indien aan, opent een klik op een externe HTTPS-link in de chat of GUI de zijbalk. Standaard uit: de meeste HTTPS-sites weigeren ingebed te worden, dus de systeembrowser is soepeler', + settingsBrowserLoopbackTitle: 'Toegestane lokale adressen', + settingsBrowserLoopbackDesc: 'Door komma's gescheiden allowlist van loopback-adressen (bijv. localhost:5174 of 127.0.0.1:8080) die de zijbalk-browser kan bezoeken; leeg blokkeert standaard alle lokale adressen. De sandbox blijft van toepassing — pagina's kunnen geen GUI-gegevens lezen', + settingsBrowserLoopbackPlaceholder: 'bijv. localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'Openen in browser', browserEmbedBlocked: '{host} weigerde ingebed te worden', browserEmbedBlockedDesc: 'De site verbiedt weergave binnen andere pagina\'s (X-Frame-Options / frame-ancestors), dus kan niet in de zijbalk laden. Open de site direct in uw browser.', diff --git a/src/client/locales-pl.ts b/src/client/locales-pl.ts index abb087d..e8224ae 100644 --- a/src/client/locales-pl.ts +++ b/src/client/locales-pl.ts @@ -262,6 +262,9 @@ export const pl: Record = { settingsBrowserHttpDesc: 'Po włączeniu kliknięcie zewnętrznego linku HTTP na czacie lub w interfejsie otwiera panel boczny (strony wtyczek deklarujące urlTarget mają pierwszeństwo); Ctrl/Cmd+klik zawsze pomija', settingsBrowserHttpsTitle: 'Otwieraj strony HTTPS w panelu bocznym', settingsBrowserHttpsDesc: 'Po włączeniu kliknięcie zewnętrznego linku HTTPS na czacie lub w interfejsie otwiera panel boczny. Domyślnie wyłączone: większość stron HTTPS odmawia osadzania, więc przeglądarka systemowa jest płynniejszym domyślnym wyborem', + settingsBrowserLoopbackTitle: 'Dozwolone adresy lokalne', + settingsBrowserLoopbackDesc: 'Rozdzielana przecinkami lista dozwolonych adresów loopback (np. localhost:5174 lub 127.0.0.1:8080), które może odwiedzać przeglądarka paska bocznego; puste domyślnie blokuje wszystkie adresy lokalne. Piaskownica nadal obowiązuje — strony nie mogą odczytywać danych GUI', + settingsBrowserLoopbackPlaceholder: 'np. localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'Otwórz w przeglądarce', browserEmbedBlocked: '{host} odmówił osadzenia', browserEmbedBlockedDesc: 'Witryna zabrania wyświetlania w innych stronach (X-Frame-Options / frame-ancestors), więc nie może się załadować w panelu bocznym. Otwórz ją bezpośrednio w przeglądarce.', diff --git a/src/client/locales-pt.ts b/src/client/locales-pt.ts index 67b866c..b80a1cd 100644 --- a/src/client/locales-pt.ts +++ b/src/client/locales-pt.ts @@ -243,6 +243,9 @@ export const pt: Record = { settingsBrowserHttpDesc: 'Quando ativado, clicar em um link externo HTTP no chat ou na interface abre na barra lateral (páginas de plugins que declaram urlTarget vencem); Ctrl/Cmd+clique sempre ignora', settingsBrowserHttpsTitle: 'Abrir páginas HTTPS na barra lateral', settingsBrowserHttpsDesc: 'Quando ativado, clicar em um link externo HTTPS no chat ou na interface abre na barra lateral. Desativado por padrão: a maioria dos sites HTTPS se recusa a ser incorporada, então o navegador do sistema é o padrão mais suave', + settingsBrowserLoopbackTitle: 'Endereços locais permitidos', + settingsBrowserLoopbackDesc: 'Lista de permissões separada por vírgulas de endereços de loopback (ex. localhost:5174 ou 127.0.0.1:8080) que o navegador da barra lateral pode visitar; vazio bloqueia todos os endereços locais por padrão. A sandbox ainda se aplica — as páginas não podem ler dados da GUI', + settingsBrowserLoopbackPlaceholder: 'ex. localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'Abrir no navegador', browserEmbedBlocked: '{host} se recusou a ser incorporado', browserEmbedBlockedDesc: 'O site proíbe ser exibido dentro de outras páginas (X-Frame-Options / frame-ancestors), então não pode carregar na barra lateral. Abra-o diretamente no navegador.', diff --git a/src/client/locales-ru.ts b/src/client/locales-ru.ts index 2020b2e..42bd5bb 100644 --- a/src/client/locales-ru.ts +++ b/src/client/locales-ru.ts @@ -257,6 +257,9 @@ export const ru: Record = { settingsBrowserHttpDesc: 'Если включено, щелчок по HTTP-ссылке в чате или интерфейсе открывает её в боковой панели (страницы плагинов с urlTarget имеют приоритет); Ctrl/Cmd+щелчок всегда пропускает', settingsBrowserHttpsTitle: 'Открывать HTTPS-страницы в боковой панели', settingsBrowserHttpsDesc: 'Если включено, щелчок по HTTPS-ссылке в чате или интерфейсе открывает её в боковой панели. По умолчанию выключено: большинство HTTPS-сайтов отказываются встраиваться, поэтому системный браузер работает плавнее', + settingsBrowserLoopbackTitle: 'Разрешённые локальные адреса', + settingsBrowserLoopbackDesc: 'Разделённый запятыми список разрешённых loopback-адресов (напр. localhost:5174 или 127.0.0.1:8080), которые может посещать браузер боковой панели; пустое значение по умолчанию блокирует все локальные адреса. Песочница продолжает действовать — страницы не могут читать данные интерфейса', + settingsBrowserLoopbackPlaceholder: 'напр. localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'Открыть в браузере', browserEmbedBlocked: '{host} отказал во встраивании', browserEmbedBlockedDesc: 'Сайт запрещает отображение внутри других страниц (X-Frame-Options / frame-ancestors), поэтому его нельзя загрузить в боковой панели. Откройте его напрямую в браузере', diff --git a/src/client/locales-sv.ts b/src/client/locales-sv.ts index cd5d6fa..887591e 100644 --- a/src/client/locales-sv.ts +++ b/src/client/locales-sv.ts @@ -243,6 +243,9 @@ export const sv: Record = { settingsBrowserHttpDesc: 'När på, klick på en extern HTTP-länk i chatten eller GUI öppnar sidopanelen (pluginsidor som deklarerar urlTarget vinner); Ctrl/Cmd+klick kringgår alltid', settingsBrowserHttpsTitle: 'Öppna HTTPS-sidor i sidopanelen', settingsBrowserHttpsDesc: 'När på, klick på en extern HTTPS-länk i chatten eller GUI öppnar sidopanelen. Av som standard: de flesta HTTPS-sajter vägrar bli inbäddade, så systemwebbläsaren är det smidigare standardvalet', + settingsBrowserLoopbackTitle: 'Tillåtna lokala adresser', + settingsBrowserLoopbackDesc: 'Kommaseparerad tillåten lista över loopback-adresser (t.ex. localhost:5174 eller 127.0.0.1:8080) som sidofältswebbläsaren kan besöka; tom blockerar alla lokala adresser som standard. Sandlådan gäller fortfarande — sidor kan inte läsa GUI-data', + settingsBrowserLoopbackPlaceholder: 't.ex. localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'Öppna i webbläsare', browserEmbedBlocked: '{host} vägrade bli inbäddad', browserEmbedBlockedDesc: 'Sajten förbjuder att visas inuti andra sidor (X-Frame-Options / frame-ancestors), så den kan inte ladda i sidopanelen. Öppna den direkt i er webbläsare i stället.', diff --git a/src/client/locales-th.ts b/src/client/locales-th.ts index f8c76cb..3b50b8b 100644 --- a/src/client/locales-th.ts +++ b/src/client/locales-th.ts @@ -260,6 +260,9 @@ export const th: Record = { settingsBrowserHttpDesc: 'เมื่อเปิด การคลิกลิงก์ HTTP ภายนอกในแชทหรือ GUI จะเปิดแถบด้านข้าง (หน้าปลั๊กอินที่ประกาศ urlTarget win มีความสำคัญก่อน); Ctrl/Cmd+click จะข้ามเสมอ', settingsBrowserHttpsTitle: 'เปิดหน้า HTTPS ในแถบด้านข้าง', settingsBrowserHttpsDesc: 'เมื่อเปิด การคลิกลิงก์ HTTPS ภายนอกในแชทหรือ GUI จะเปิดแถบด้านข้าง ปิดเป็นค่าเริ่มต้น: ไซต์ HTTPS ส่วนใหญ่ปฏิเสธที่จะฝัง ดังนั้นเบราว์เซอร์ระบบจะราบรื่นกว่าเป็นค่าเริ่มต้น', + settingsBrowserLoopbackTitle: 'ที่อยู่ภายในเครื่องที่อนุญาต', + settingsBrowserLoopbackDesc: 'รายการที่อนุญาตแบบคั่นด้วยจุลภาคของที่อยู่ลูปแบ็ก (เช่น localhost:5174 หรือ 127.0.0.1:8080) ที่เบราว์เซอร์แถบด้านข้างสามารถเยี่ยมชมได้ ค่าว่างจะบล็อกที่อยู่ภายในทั้งหมดโดยค่าเริ่มต้น แซนด์บ็อกซ์ยังคงมีผล — หน้าเว็บไม่สามารถอ่านข้อมูล GUI ได้', + settingsBrowserLoopbackPlaceholder: 'เช่น localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'เปิดในเบราว์เซอร์', browserEmbedBlocked: '{host} ปฏิเสธที่จะถูกฝัง', browserEmbedBlockedDesc: 'ไซต์ห้ามไม่ให้แสดงภายในหน้าอื่น (X-Frame-Options / frame-ancestors) จึงไม่สามารถโหลดในแถบด้านข้างได้ เปิดโดยตรงในเบราว์เซอร์ของคุณแทน', diff --git a/src/client/locales-tr.ts b/src/client/locales-tr.ts index 866dce5..3e03d08 100644 --- a/src/client/locales-tr.ts +++ b/src/client/locales-tr.ts @@ -260,6 +260,9 @@ export const tr: Record = { settingsBrowserHttpDesc: 'Açıkken sohbetteki veya arayüzdeki bir HTTP dış bağlantısına tıklamak kenar çubuğunu açar (urlTarget bildiren eklenti sayfaları öncelikli); Ctrl/Cmd+tıklama her zaman atlatır', settingsBrowserHttpsTitle: 'HTTPS sayfalarını kenar çubuğunda aç', settingsBrowserHttpsDesc: 'Açıkken sohbetteki veya arayüzdeki bir HTTPS dış bağlantısına tıklamak kenar çubuğunu açar. Varsayılan kapalı: çoğu HTTPS sitesi gömülmeyi reddeder, bu yüzden sistem tarayıcısı daha akıcı varsayılandır', + settingsBrowserLoopbackTitle: 'İzin verilen yerel adresler', + settingsBrowserLoopbackDesc: 'Kenar çubuğu tarayıcısının ziyaret edebileceği geri döngü adreslerinin virgülle ayrılmış izin listesi (ör. localhost:5174 veya 127.0.0.1:8080); boş, varsayılan olarak tüm yerel adresleri engeller. Kum havuzu hala geçerlidir — sayfalar GUI verilerini okuyamaz', + settingsBrowserLoopbackPlaceholder: 'ör. localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'Tarayıcıda aç', browserEmbedBlocked: '{host} gömülmeyi reddetti', browserEmbedBlockedDesc: 'Site başka sayfalar içinde gösterilmeyi yasaklıyor (X-Frame-Options / frame-ancestors), bu yüzden kenar çubuğunda yüklenemiyor. Bunun yerine doğrudan tarayıcınızda açın.', diff --git a/src/client/locales-vi.ts b/src/client/locales-vi.ts index 588ec72..596f530 100644 --- a/src/client/locales-vi.ts +++ b/src/client/locales-vi.ts @@ -260,6 +260,9 @@ export const vi: Record = { settingsBrowserHttpDesc: 'Khi bật, nhấp link HTTP ngoài trong chat hoặc UI sẽ mở trong thanh bên (trang plugin khai báo urlTarget ưu tiên); Ctrl/Cmd+nhấp luôn bỏ qua', settingsBrowserHttpsTitle: 'Mở trang HTTPS trong thanh bên', settingsBrowserHttpsDesc: 'Khi bật, nhấp link HTTPS ngoài trong chat hoặc UI sẽ mở trong thanh bên. Mặc định tắt: đa số trang HTTPS từ chối nhúng, trình duyệt hệ thống mượt mà hơn', + settingsBrowserLoopbackTitle: 'Địa chỉ cục bộ được phép', + settingsBrowserLoopbackDesc: 'Danh sách cho phép phân tách bằng dấu phẩy của địa chỉ loopback (vd. localhost:5174 hoặc 127.0.0.1:8080) mà trình duyệt thanh bên có thể truy cập; để trống sẽ chặn tất cả địa chỉ cục bộ theo mặc định. Hộp cát vẫn áp dụng — các trang không thể đọc dữ liệu GUI', + settingsBrowserLoopbackPlaceholder: 'vd. localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'Mở trong trình duyệt', browserEmbedBlocked: '{host} từ chối nhúng', browserEmbedBlockedDesc: 'Trang này cấm hiển thị trong trang khác qua X-Frame-Options / frame-ancestors, không thể tải trong thanh bên. Mở trực tiếp trong trình duyệt', diff --git a/src/client/locales-zh-HK.ts b/src/client/locales-zh-HK.ts index 1e89d83..774836e 100644 --- a/src/client/locales-zh-HK.ts +++ b/src/client/locales-zh-HK.ts @@ -275,6 +275,9 @@ export const zhHK: Record = { settingsBrowserHttpDesc: '開啟後,點擊聊天或介面中的 HTTP 外鏈時在側邊欄開啟(宣告了 urlTarget 的插件頁面優先);Ctrl/Cmd 點擊可臨時放行', settingsBrowserHttpsTitle: '側邊開啟HTTPS網頁', settingsBrowserHttpsDesc: '開啟後,點擊聊天或介面中的 HTTPS 外鏈時在側邊欄開啟。預設關閉:多數 HTTPS 網站拒絕被嵌入,走系統瀏覽器更順暢', + settingsBrowserLoopbackTitle: '允許訪問的本機地址', + settingsBrowserLoopbackDesc: '逗號分隔的本機回環地址白名單(如 localhost:5174 或 127.0.0.1:8080),側邊欄瀏覽器可訪問這些本機服務;預設留空則本機地址全部攔截。沙箱隔離仍然生效,頁面無法讀取介面資料', + settingsBrowserLoopbackPlaceholder: '例如 localhost:5174, 127.0.0.1:8080', browserOpenExternal: '在瀏覽器中開啟', browserEmbedBlocked: '{host} 拒絕了嵌入請求', browserEmbedBlockedDesc: '該網站透過 X-Frame-Options / frame-ancestors 禁止在其它頁面中顯示,無法在側邊欄內載入。可在瀏覽器中直接開啟', diff --git a/src/client/locales-zh-MO.ts b/src/client/locales-zh-MO.ts index c08336e..0ce95dc 100644 --- a/src/client/locales-zh-MO.ts +++ b/src/client/locales-zh-MO.ts @@ -275,6 +275,9 @@ export const zhMO: Record = { settingsBrowserHttpDesc: '開啟後,點擊聊天或介面中的 HTTP 外鏈時在側邊欄開啟(宣告了 urlTarget 的插件頁面優先);Ctrl/Cmd 點擊可臨時放行', settingsBrowserHttpsTitle: '側邊開啟HTTPS網頁', settingsBrowserHttpsDesc: '開啟後,點擊聊天或介面中的 HTTPS 外鏈時在側邊欄開啟。預設關閉:多數 HTTPS 網站拒絕被嵌入,走系統瀏覽器更順暢', + settingsBrowserLoopbackTitle: '允許訪問的本機地址', + settingsBrowserLoopbackDesc: '逗號分隔的本機回環地址白名單(如 localhost:5174 或 127.0.0.1:8080),側邊欄瀏覽器可訪問這些本機服務;預設留空則本機地址全部攔截。沙箱隔離仍然生效,頁面無法讀取介面資料', + settingsBrowserLoopbackPlaceholder: '例如 localhost:5174, 127.0.0.1:8080', browserOpenExternal: '在瀏覽器中開啟', browserEmbedBlocked: '{host} 拒絕了嵌入請求', browserEmbedBlockedDesc: '該網站透過 X-Frame-Options / frame-ancestors 禁止在其它頁面中顯示,無法在側邊欄內載入。可在瀏覽器中直接開啟', diff --git a/src/client/locales-zh-TW.ts b/src/client/locales-zh-TW.ts index 8c76385..0c87491 100644 --- a/src/client/locales-zh-TW.ts +++ b/src/client/locales-zh-TW.ts @@ -275,6 +275,9 @@ export const zhTW: Record = { settingsBrowserHttpDesc: '開啟後,點擊聊天或介面中的 HTTP 外鏈時在側邊欄開啟(宣告了 urlTarget 的插件頁面優先);Ctrl/Cmd 點擊可臨時放行', settingsBrowserHttpsTitle: '側邊開啟HTTPS網頁', settingsBrowserHttpsDesc: '開啟後,點擊聊天或介面中的 HTTPS 外鏈時在側邊欄開啟。預設關閉:多數 HTTPS 網站拒絕被嵌入,走系統瀏覽器更順暢', + settingsBrowserLoopbackTitle: '允許存取的本機地址', + settingsBrowserLoopbackDesc: '逗號分隔的本機回環地址白名單(如 localhost:5174 或 127.0.0.1:8080),側邊欄瀏覽器可存取這些本機服務;預設留空則本機地址全部攔截。沙箱隔離仍然生效,頁面無法讀取介面資料', + settingsBrowserLoopbackPlaceholder: '例如 localhost:5174, 127.0.0.1:8080', browserOpenExternal: '在瀏覽器中開啟', browserEmbedBlocked: '{host} 拒絕了嵌入請求', browserEmbedBlockedDesc: '該網站透過 X-Frame-Options / frame-ancestors 禁止在其它頁面中顯示,無法在側邊欄內載入。可在瀏覽器中直接開啟', diff --git a/src/client/locales.ts b/src/client/locales.ts index 1eb8b34..2efe5c2 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -267,6 +267,9 @@ export const zh = { settingsBrowserHttpDesc: '开启后,点击聊天或界面中的 HTTP 外链时在侧边栏打开(声明了 urlTarget 的插件页面优先);Ctrl/Cmd 点击可临时放行', settingsBrowserHttpsTitle: '侧边打开HTTPS网页', settingsBrowserHttpsDesc: '开启后,点击聊天或界面中的 HTTPS 外链时在侧边栏打开。默认关闭:多数 HTTPS 站点拒绝被嵌入,走系统浏览器更顺畅', + settingsBrowserLoopbackTitle: '允许访问的本机地址', + settingsBrowserLoopbackDesc: '逗号分隔的本地回环地址白名单(如 localhost:5174 或 127.0.0.1:8080),侧边栏浏览器可访问这些本地服务;默认留空则本机地址全部拦截。沙箱隔离仍然生效,页面无法读取界面数据', + settingsBrowserLoopbackPlaceholder: '例如 localhost:5174, 127.0.0.1:8080', browserOpenExternal: '在浏览器中打开', browserEmbedBlocked: '{host} 拒绝了嵌入请求', browserEmbedBlockedDesc: '该站点通过 X-Frame-Options / frame-ancestors 禁止在其它页面中显示,无法在侧边栏内加载。可在浏览器中直接打开', @@ -601,6 +604,9 @@ export const en: Record = { settingsBrowserHttpDesc: 'When on, clicking an HTTP external link in the chat or GUI opens the sidebar (plugin pages declaring urlTarget win); Ctrl/Cmd+click always bypasses', settingsBrowserHttpsTitle: 'Open HTTPS pages in the sidebar', settingsBrowserHttpsDesc: 'When on, clicking an HTTPS external link in the chat or GUI opens the sidebar. Off by default: most HTTPS sites refuse to be embedded, so the system browser is the smoother default', + settingsBrowserLoopbackTitle: 'Allowed local addresses', + settingsBrowserLoopbackDesc: 'Comma-separated allowlist of loopback addresses (e.g. localhost:5174 or 127.0.0.1:8080) the sidebar browser may visit; empty blocks all local addresses by default. The sandbox still applies — pages cannot read GUI data', + settingsBrowserLoopbackPlaceholder: 'e.g. localhost:5174, 127.0.0.1:8080', browserOpenExternal: 'Open in browser', browserEmbedBlocked: '{host} refused to be embedded', browserEmbedBlockedDesc: 'The site forbids being displayed inside other pages (X-Frame-Options / frame-ancestors), so it cannot load in the sidebar. Open it directly in your browser instead.', diff --git a/src/client/prefs.ts b/src/client/prefs.ts index 02f33a2..f062086 100644 --- a/src/client/prefs.ts +++ b/src/client/prefs.ts @@ -119,6 +119,9 @@ export function parsePrefs(value: unknown): SidebarPrefs { browserInterceptHttps: typeof record.browserInterceptHttps === 'boolean' ? record.browserInterceptHttps : SIDEBAR_PREFS_DEFAULTS.browserInterceptHttps, + browserAllowedLoopback: typeof record.browserAllowedLoopback === 'string' + ? record.browserAllowedLoopback + : SIDEBAR_PREFS_DEFAULTS.browserAllowedLoopback, tabsEnabled: booleanMapOf(record.tabsEnabled), viewersEnabled: booleanMapOf(record.viewersEnabled), pluginSettings: pluginSettingsMapOf(record.pluginSettings), diff --git a/src/config.ts b/src/config.ts index 5f4cae0..66dc7a9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -140,6 +140,7 @@ export const PrefsSchema: z = z.object({ browserInterceptLinks: z.boolean().default(true), browserInterceptHttp: z.boolean().default(true), browserInterceptHttps: z.boolean().default(false), + browserAllowedLoopback: z.string().default(''), // Per-feature enable switches are OPEN maps (any tab/viewer id, built-in or // external): an absent key means enabled, so old documents resolve to {} // (everything on) with no migration. Non-boolean values fail validation. diff --git a/src/index.ts b/src/index.ts index f0c490a..cfdfe46 100644 --- a/src/index.ts +++ b/src/index.ts @@ -232,6 +232,26 @@ function shellOverridesOf(getSettings: () => SidebarSettingsFace | undefined): { } } +/** + * Parse the browser tab's `browserAllowedLoopback` allowlist into a matcher + * over host:port (same contract as the client-side helper in + * src/client/browser.ts — kept in sync). Bare hosts (`localhost`, + * `127.0.0.1`) match every port; `host:port` entries match exactly. + */ +function parseLoopbackAllowlist(allowlist: string): (host: string, port: string) => boolean { + const entries = allowlist.split(',').map(entry => entry.trim().toLowerCase()).filter(entry => entry !== '') + const exact = new Set(entries) + const hosts = new Set() + for (const entry of entries) { + if (!entry.includes(':')) hosts.add(entry.replace(/^\[|\]$/g, '')) + } + return (host, port) => { + const key = `${host}:${port}` + if (exact.has(key) || exact.has(host)) return true + return port !== '' && hosts.has(host) + } +} + function buildApi( ctx: Context, ptyManager: PtyManager | null, @@ -488,9 +508,16 @@ function buildApi( throw new SidebarError('bad-request', 'only http/https urls can be probed', 400) } // Mirror the browser tab's address-bar policy: loopback stays unreachable - // from the sidebar, so probing it would leak nothing the tab could use. + // from the sidebar (unless the user allowlisted it), so probing it would + // leak nothing the tab could use. if (isLoopbackHostname(parsed.hostname)) { - throw new SidebarError('bad-request', 'local addresses are not probed', 400) + const prefs = getSettings()?.get()?.value as SidebarPrefs | undefined + const allowlist = typeof prefs?.browserAllowedLoopback === 'string' ? prefs.browserAllowedLoopback : '' + const allowed = allowlist.trim() !== '' + && parseLoopbackAllowlist(allowlist)(parsed.hostname, parsed.port) + if (!allowed) { + throw new SidebarError('bad-request', 'local addresses are not probed', 400) + } } const controller = new AbortController() const timer = setTimeout(() => controller.abort(), 8000) @@ -501,6 +528,17 @@ function buildApi( if (response.status === 405 || response.status === 501) { response = await fetch(parsed, { method: 'GET', redirect: 'follow', signal: controller.signal }) } + // Some servers (e.g. aliyun consoles) answer HEAD without the + // X-Frame-Options / CSP headers that only their GET response + // carries. Without those signals the embeddability check below + // would wrongly report the site as embeddable and the plain iframe + // would surface the browser's misleading "refused to connect". + // Retry once as GET when both signals are absent (body discarded). + const hasEmbedSignals = response.headers.get('content-security-policy') !== null + || response.headers.get('x-frame-options') !== null + if (!hasEmbedSignals && response.status !== 405 && response.status !== 501) { + response = await fetch(parsed, { method: 'GET', redirect: 'follow', signal: controller.signal }) + } const csp = response.headers.get('content-security-policy') const frameAncestors = extractFrameAncestors(csp) const xFrameOptions = response.headers.get('x-frame-options') diff --git a/src/prefs-shared.ts b/src/prefs-shared.ts index 027998d..551978f 100644 --- a/src/prefs-shared.ts +++ b/src/prefs-shared.ts @@ -175,6 +175,16 @@ export interface SidebarPrefs { * tab's own enable switch. */ browserInterceptHttps: boolean + /** + * Comma-separated allowlist of local (loopback) authorities the browser + * tab may navigate to — `localhost`, `127.0.0.1`, `127.0.0.1:5174`, or + * host:port pairs. Empty by default: loopback addresses stay blocked so a + * browsed page cannot probe local services. Each entry is either a bare + * hostname (all ports) or host:port; the GUI's own origin is always + * allowed regardless. The iframe sandbox still renders allowed local + * pages in an opaque origin, exactly like any other site. + */ + browserAllowedLoopback: string /** * Per-tab enable switches, keyed by tab descriptor id (`'explorer'`, * `'my-plugin:db'`). An ABSENT key means enabled — only an explicit @@ -247,6 +257,7 @@ export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = { browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, + browserAllowedLoopback: '', tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {},