feat: browser-side annotation screenshots for web preview

Capture the preview/browser iframe DOM with snapDOM (html-to-image
fallback) so web annotation screenshots match the visible viewport,
without a headless Chromium dependency.

- Preserve document scroll via viewport crop and re-bake nested scroll
  (e.g. the Starlight sidebar) deterministically on the clone
- Pin position:fixed elements to their measured viewport rect so headers
  and sidebars land correctly in the crop
- Extract preview capture/proxy helpers into
  lib/preview/screenshot-capture.ts to slim down ContextPanel
- Guard the external preview proxy against SSRF to private, loopback and
  reserved/link-local addresses (incl. cloud metadata)
- Fully validate preview bridge messages before formatting/use
- Warn on the empty browser tab that pages run with full access, so
  users browse untrusted sites knowingly
This commit is contained in:
Bohdan Triapitsyn
2026-05-30 02:04:32 +03:00
parent 49ed0b52c9
commit 7f90ffb878
15 changed files with 1591 additions and 210 deletions
+2
View File
@@ -842,6 +842,8 @@ export const dict = {
'contextPanel.browser.addressAria': 'Browser address',
'contextPanel.browser.empty': 'Web browser',
'contextPanel.browser.emptyHint': 'Enter an address above to start browsing the web',
'contextPanel.browser.inspectUnavailable': 'This page cannot be inspected from the browser panel.',
'contextPanel.browser.trustNotice': 'Pages opened here run with full access to OpenChamber — needed for inspect and screenshots. Only open sites you trust: a malicious page could read your data or act on your behalf.',
'contextPanel.tab.closeTabAria': 'Close {label} tab',
'contextPanel.actions.collapsePanel': 'Collapse panel',
'contextPanel.actions.expandPanel': 'Expand panel',
+2
View File
@@ -843,6 +843,8 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.browser.addressAria": "Dirección del navegador",
"contextPanel.browser.empty": "Navegador web",
"contextPanel.browser.emptyHint": "Ingrese una dirección arriba para comenzar a navegar",
"contextPanel.browser.inspectUnavailable": "Esta página no se puede inspeccionar desde el panel del navegador.",
"contextPanel.browser.trustNotice": "Las páginas que abras aquí se ejecutan con acceso completo a OpenChamber: necesario para la inspección y las capturas. Abre solo sitios de confianza: una página maliciosa podría leer tus datos o actuar en tu nombre.",
"contextPanel.tab.closeTabAria": "Cerrar pestaña {label}",
"contextPanel.actions.collapsePanel": "Colapsar panel",
"contextPanel.actions.expandPanel": "Expandir panel",
+2
View File
@@ -843,6 +843,8 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.browser.addressAria': '브라우저 주소',
'contextPanel.browser.empty': '웹 브라우저',
'contextPanel.browser.emptyHint': '위에 주소를 입력하여 탐색을 시작하세요',
'contextPanel.browser.inspectUnavailable': '브라우저 패널에서 이 페이지를 검사할 수 없습니다.',
'contextPanel.browser.trustNotice': '여기서 여는 페이지는 OpenChamber에 대한 전체 액세스 권한으로 실행됩니다 — 검사와 스크린샷에 필요합니다. 신뢰하는 사이트만 여세요: 악성 페이지가 데이터를 읽거나 사용자를 대신해 동작할 수 있습니다.',
'contextPanel.preview.actions.reload': '미리보기 새로고침',
'contextPanel.preview.actions.openExternal': '브라우저에서 열기',
'contextPanel.preview.actions.retry': '다시 시도',
+2
View File
@@ -1094,6 +1094,8 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.browser.addressAria': 'Adres przeglądarki',
'contextPanel.browser.empty': 'Przeglądarka internetowa',
'contextPanel.browser.emptyHint': 'Wprowadź adres powyżej, aby rozpocząć przeglądanie',
'contextPanel.browser.inspectUnavailable': 'Nie można sprawdzić tej strony z panelu przeglądarki.',
'contextPanel.browser.trustNotice': 'Strony otwierane tutaj działają z pełnym dostępem do OpenChamber — jest to wymagane do inspekcji i zrzutów ekranu. Otwieraj tylko zaufane witryny: złośliwa strona może odczytać Twoje dane lub działać w Twoim imieniu.',
'contextPanel.preview.actions.openExternal': 'Otwórz w przeglądarce',
'contextPanel.preview.actions.reload': 'Odśwież podgląd',
'contextPanel.preview.actions.retry': 'Ponów',
@@ -843,6 +843,8 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.browser.addressAria": "Endereço do navegador",
"contextPanel.browser.empty": "Navegador web",
"contextPanel.browser.emptyHint": "Digite um endereço acima para começar a navegar",
"contextPanel.browser.inspectUnavailable": "Esta página não pode ser inspecionada pelo painel do navegador.",
"contextPanel.browser.trustNotice": "As páginas abertas aqui são executadas com acesso total ao OpenChamber — necessário para inspeção e capturas de tela. Abra apenas sites confiáveis: uma página maliciosa pode ler seus dados ou agir em seu nome.",
"contextPanel.tab.closeTabAria": "Fechar aba {label}",
"contextPanel.actions.collapsePanel": "Recolher painel",
"contextPanel.actions.expandPanel": "Expandir painel",
+2
View File
@@ -843,6 +843,8 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.browser.addressAria": "Адреса браузера",
"contextPanel.browser.empty": "Веб-браузер",
"contextPanel.browser.emptyHint": "Введіть адресу вище, щоб почати перегляд",
"contextPanel.browser.inspectUnavailable": "Цю сторінку неможливо інспектувати з панелі браузера.",
"contextPanel.browser.trustNotice": "Сторінки, відкриті тут, працюють із повним доступом до OpenChamber — це потрібно для inspect і скріншотів. Відкривайте лише сайти, яким довіряєте: шкідлива сторінка може прочитати ваші дані чи діяти від вашого імені.",
"contextPanel.tab.closeTabAria": "Закрити вкладку {label}",
"contextPanel.actions.collapsePanel": "Згорнути панель",
"contextPanel.actions.expandPanel": "Розгорнути панель",
@@ -843,6 +843,8 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.browser.addressAria': '浏览器地址',
'contextPanel.browser.empty': '网页浏览器',
'contextPanel.browser.emptyHint': '在上方输入网址开始浏览',
'contextPanel.browser.inspectUnavailable': '无法从浏览器面板检查此页面。',
'contextPanel.browser.trustNotice': '在此打开的页面以对 OpenChamber 的完全访问权限运行 — 检查和截图需要此权限。仅打开你信任的站点:恶意页面可能读取你的数据或以你的身份执行操作。',
'contextPanel.tab.closeTabAria': '关闭 {label} 标签',
'contextPanel.actions.collapsePanel': '折叠面板',
'contextPanel.actions.expandPanel': '展开面板',
@@ -842,6 +842,8 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.browser.addressAria': '瀏覽器網址',
'contextPanel.browser.empty': '網頁瀏覽器',
'contextPanel.browser.emptyHint': '在上方輸入網址開始瀏覽',
'contextPanel.browser.inspectUnavailable': '無法從瀏覽器面板檢查此頁面。',
'contextPanel.browser.trustNotice': '在此開啟的頁面以對 OpenChamber 的完整存取權限執行 — 檢查與截圖需要此權限。僅開啟你信任的網站:惡意頁面可能讀取你的資料或以你的身分執行操作。',
'contextPanel.tab.closeTabAria': '關閉 {label} 分頁',
'contextPanel.actions.collapsePanel': '摺疊面板',
'contextPanel.actions.expandPanel': '展開面板',