From 5141100ea0a61420e6376c349f447a964594091d Mon Sep 17 00:00:00 2001
From: Baivab Sarkar <109382325+ThisIs-Developer@users.noreply.github.com>
Date: Sat, 14 Nov 2026 09:50:56 +0530
Subject: [PATCH] feat(security): encrypt snapshots, media, and live sharing
---
Dockerfile | 4 +
README.md | 10 +-
assets/i18n/bg.json | 4 +
assets/i18n/de.json | 4 +
assets/i18n/en.json | 4 +
assets/i18n/es.json | 4 +
assets/i18n/fr.json | 4 +
assets/i18n/it.json | 4 +
assets/i18n/ja.json | 4 +
assets/i18n/ko.json | 4 +
assets/i18n/pl.json | 4 +
assets/i18n/pt.json | 4 +
assets/i18n/ru.json | 4 +
assets/i18n/tr.json | 4 +
assets/i18n/tw.json | 4 +
assets/i18n/uk.json | 4 +
assets/i18n/zh.json | 4 +
assets/yjs.LICENSE.txt | 47 +
assets/yjs.mjs | 5 +
desktop-app/prepare.js | 27 +-
desktop-app/resources/index.html | 5 +
desktop-app/resources/js/script.js | 284 +++--
desktop-app/resources/js/sharing-crypto.js | 207 ++++
desktop-app/resources/js/sharing-live.js | 129 ++
desktop-app/resources/js/sharing-media.js | 139 +++
docs/ENCRYPTED_SHARING.md | 45 +
functions/api/image/[[id]].js | 78 +-
functions/api/share/[[id]].js | 53 +-
functions/live-room/[[room]].js | 12 +-
index.html | 5 +
package-lock.json | 1167 ++++++++++++++++++-
package.json | 11 +-
script.js | 284 +++--
scripts/build-sharing.mjs | 7 +
sharing-crypto.js | 207 ++++
sharing-live.js | 129 ++
sharing-media.js | 139 +++
sw.js | 13 +-
tests/e2e/encrypted-sharing.spec.js | 161 +++
tests/e2e/native-drop-safety.spec.js | 7 +-
tests/e2e/share-live.spec.js | 2 +-
tests/e2e/slash-command-actions.spec.js | 10 +-
tests/helpers/app.js | 9 +-
tests/helpers/encrypted-sharing-runtime.mjs | 14 +
tests/helpers/encrypted-sharing-worker.mjs | 26 +
tests/helpers/static-build-check.mjs | 23 +-
tests/unit/sharing-encryption.test.mjs | 198 ++++
tests/unit/sharing-relay.test.mjs | 94 ++
wiki/Configuration.md | 29 +-
wiki/FAQ.md | 10 +-
wiki/Live-Share-Cloudflare.md | 111 +-
wiki/Privacy-and-Security.md | 62 +-
wiki/Share-Snapshot.md | 128 +-
wiki/Troubleshooting.md | 8 +-
wiki/Usage-Guide.md | 8 +-
workers/live-room-worker.js | 122 +-
56 files changed, 3484 insertions(+), 605 deletions(-)
create mode 100644 assets/yjs.LICENSE.txt
create mode 100644 assets/yjs.mjs
create mode 100644 desktop-app/resources/js/sharing-crypto.js
create mode 100644 desktop-app/resources/js/sharing-live.js
create mode 100644 desktop-app/resources/js/sharing-media.js
create mode 100644 docs/ENCRYPTED_SHARING.md
create mode 100644 scripts/build-sharing.mjs
create mode 100644 sharing-crypto.js
create mode 100644 sharing-live.js
create mode 100644 sharing-media.js
create mode 100644 tests/e2e/encrypted-sharing.spec.js
create mode 100644 tests/helpers/encrypted-sharing-runtime.mjs
create mode 100644 tests/helpers/encrypted-sharing-worker.mjs
create mode 100644 tests/unit/sharing-encryption.test.mjs
create mode 100644 tests/unit/sharing-relay.test.mjs
diff --git a/Dockerfile b/Dockerfile
index 8c028681..40204ec1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,6 +7,7 @@ COPY workspace-storage.js /usr/share/nginx/html/
COPY visual-settings-adapters.js /usr/share/nginx/html/
COPY visual-settings-store.js /usr/share/nginx/html/
COPY script.js /usr/share/nginx/html/
+COPY sharing-crypto.js sharing-media.js sharing-live.js /usr/share/nginx/html/
COPY styles.css /usr/share/nginx/html/
COPY sw.js /usr/share/nginx/html/
COPY RELEASE_NOTES /usr/share/nginx/html/
@@ -39,6 +40,9 @@ RUN echo 'server { \
} \
\
# Cache static assets \
+ location ~* \.mjs$ { \
+ default_type application/javascript; \
+ } \
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { \
expires 1y; \
add_header Cache-Control "public, immutable"; \
diff --git a/README.md b/README.md
index 5e4a3b16..85e2cc9e 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@
Markdown Viewer is an open source, local-first workspace for developers, writers, students, researchers, and anyone working with `.md` or `.markdown` files. Go beyond plain text. Organize multiple documents, review with anchored comments without changing the Markdown, and render diagrams, maps, charts, math, 3D models, and music—all in one focused workspace.
-Turn a document into a Share Snapshot link for quick handoffs, or start access-controlled Live Share for real-time co-editing, live cursors, anchored comments, and threaded replies. Everyday editing stays on your device. No account is required, and the application includes no ads, analytics, telemetry, or subscriptions.
+Turn a document into an encrypted Share Snapshot link for quick handoffs, or start encrypted Live Share for real-time co-editing, live cursors, anchored comments, and threaded replies. Everyday editing stays on your device. No account is required, and the application includes no ads, analytics, telemetry, or subscriptions.
@@ -74,7 +74,7 @@ Turn a document into a Share Snapshot link for quick handoffs, or start access-c
- **Import and export:** open local files or GitHub content from branches, tags, commit SHAs, and optionally private repositories; export Markdown, standalone HTML, PNG, Standard PDF/Save as PDF, or Rendered PDF with remembered Light or Dark appearance and complete rich-content preparation for PDF and PNG.
-- **Optional sharing:** create a Share Snapshot with View only or Can edit access, or start a temporary Live Share room with host, Can edit, and View only capabilities.
+- **Optional sharing:** create an encrypted Share Snapshot with View only or Can edit access, or start an encrypted Live Share room with role-based signatures. Keep complete links private: they carry decryption keys. Existing shares retain their original expiry.
@@ -115,12 +115,12 @@ Markdown Viewer is local-first, not network-free. The following table shows the
| Web libraries and uncached renderer dependencies | CDN requests in the web/PWA build |
| GitHub import and emoji lookup | Public content uses GitHub APIs and raw-content hosts; private content uses an optional PAT only with `api.github.com` |
| PlantUML, D2, Graphviz, Vega-Lite, WaveDrom, and some diagram previews | Diagram source can be sent to PlantUML, Kroki, or mermaid.ink |
-| Consented image, GIF, and video insertion | Public, content-addressed Cloudflare media storage for 90 days |
+| Consented image, GIF, and video insertion | Encrypted Cloudflare media storage for 90 days |
| Large Share Snapshot | Cloudflare KV for 90 days |
| Live Share | WebSocket relay through a Cloudflare Durable Object; document content is not persisted server-side |
| External images, media, links, and map tiles | The referenced external host |
-Share Snapshot and Live Share URLs are bearer links. Anyone who obtains a valid link and capability can use the access it grants. Live Share is not end-to-end encrypted. See [Privacy and Security](wiki/Privacy-and-Security.md) before using network features with sensitive content.
+Share Snapshot and Live Share URLs are bearer links. Anyone who obtains a valid link and capability can use the access it grants. New snapshots, managed media, and live rooms use client-side encryption; complete links contain decryption keys. See [Privacy and Security](wiki/Privacy-and-Security.md) before using network features with sensitive content.
## Visual Renderer Summary
@@ -164,7 +164,7 @@ Remote renderer services receive the source of the diagram they render. Do not s
- A local Markdown file larger than 10 MB is rejected.
- The GitHub credential vault stores up to 50 named PAT entries; each token name is limited to 60 characters.
- Managed source media is limited to 25 MiB before processing; stored payload limits are 300 KiB for still images, 5 MiB for GIFs, and 10 MiB for videos.
-- Stored Share Snapshot content is limited to 8,000,000 characters and expires after 90 days.
+- Stored Share Snapshot payloads are limited to 8 MiB before encryption and expire after 90 days.
- Live Share allows up to 64 WebSocket participants and 8 MB per live message.
- STL source is limited to 2 MiB and rendered geometry to 300,000 vertices.
- The desktop vault retains up to 20 recent history copies per document.
diff --git a/assets/i18n/bg.json b/assets/i18n/bg.json
index 90c23ca1..e0723545 100644
--- a/assets/i18n/bg.json
+++ b/assets/i18n/bg.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "Шифровано споделяне. Пазете цялата връзка в тайна; тя съдържа ключа за дешифриране.",
+ "Delete snapshot": "Изтриване на моментната снимка",
+ "Snapshot deleted.": "Моментната снимка е изтрита.",
+ "Connected to encrypted live room": "Свързано с шифрована стая на живо",
"Linking folder": "Свързване на папка",
"Scanning folder…": "Сканиране на папката…",
"Reading files…": "Четене на файлове…",
diff --git a/assets/i18n/de.json b/assets/i18n/de.json
index 82d8db05..f3ca960a 100644
--- a/assets/i18n/de.json
+++ b/assets/i18n/de.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "Verschlüsselte Freigabe. Halten Sie den vollständigen Link geheim; er enthält den Entschlüsselungsschlüssel.",
+ "Delete snapshot": "Momentaufnahme löschen",
+ "Snapshot deleted.": "Momentaufnahme gelöscht.",
+ "Connected to encrypted live room": "Mit verschlüsseltem Live-Raum verbunden",
"Linking folder": "Ordner wird verknüpft",
"Scanning folder…": "Ordner wird durchsucht…",
"Reading files…": "Dateien werden gelesen…",
diff --git a/assets/i18n/en.json b/assets/i18n/en.json
index 74f011b4..2a70d56e 100644
--- a/assets/i18n/en.json
+++ b/assets/i18n/en.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "Encrypted sharing. Keep the complete link private; it includes the decryption key.",
+ "Delete snapshot": "Delete snapshot",
+ "Snapshot deleted.": "Snapshot deleted.",
+ "Connected to encrypted live room": "Connected to encrypted live room",
"Linking folder": "Linking folder",
"Scanning folder…": "Scanning folder…",
"Reading files…": "Reading files…",
diff --git a/assets/i18n/es.json b/assets/i18n/es.json
index 95a5e8fb..0e8c1429 100644
--- a/assets/i18n/es.json
+++ b/assets/i18n/es.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "Uso compartido cifrado. Mantén privado el enlace completo; incluye la clave de descifrado.",
+ "Delete snapshot": "Eliminar instantánea",
+ "Snapshot deleted.": "Instantánea eliminada.",
+ "Connected to encrypted live room": "Conectado a la sala en vivo cifrada",
"Linking folder": "Vinculando carpeta",
"Scanning folder…": "Explorando carpeta…",
"Reading files…": "Leyendo archivos…",
diff --git a/assets/i18n/fr.json b/assets/i18n/fr.json
index c7faefe7..e8e9d69e 100644
--- a/assets/i18n/fr.json
+++ b/assets/i18n/fr.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "Partage chiffré. Gardez le lien complet privé ; il contient la clé de déchiffrement.",
+ "Delete snapshot": "Supprimer l’instantané",
+ "Snapshot deleted.": "Instantané supprimé.",
+ "Connected to encrypted live room": "Connecté à la salle en direct chiffrée",
"Linking folder": "Liaison du dossier",
"Scanning folder…": "Analyse du dossier…",
"Reading files…": "Lecture des fichiers…",
diff --git a/assets/i18n/it.json b/assets/i18n/it.json
index 62ef95e6..b7d7b4e9 100644
--- a/assets/i18n/it.json
+++ b/assets/i18n/it.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "Condivisione crittografata. Mantieni privato il link completo: contiene la chiave di decrittazione.",
+ "Delete snapshot": "Elimina istantanea",
+ "Snapshot deleted.": "Istantanea eliminata.",
+ "Connected to encrypted live room": "Connesso alla stanza live crittografata",
"Linking folder": "Collegamento della cartella",
"Scanning folder…": "Scansione della cartella…",
"Reading files…": "Lettura dei file…",
diff --git a/assets/i18n/ja.json b/assets/i18n/ja.json
index f97aa7f4..9ee8ff3b 100644
--- a/assets/i18n/ja.json
+++ b/assets/i18n/ja.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "暗号化された共有です。完全なリンクには復号キーが含まれるため、秘密にしてください。",
+ "Delete snapshot": "スナップショットを削除",
+ "Snapshot deleted.": "スナップショットを削除しました。",
+ "Connected to encrypted live room": "暗号化されたライブルームに接続しました",
"Linking folder": "フォルダーをリンク中",
"Scanning folder…": "フォルダーをスキャン中…",
"Reading files…": "ファイルを読み込み中…",
diff --git a/assets/i18n/ko.json b/assets/i18n/ko.json
index bea0a9bd..ce39c79e 100644
--- a/assets/i18n/ko.json
+++ b/assets/i18n/ko.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "암호화된 공유입니다. 전체 링크에는 복호화 키가 포함되어 있으므로 비공개로 유지하세요.",
+ "Delete snapshot": "스냅샷 삭제",
+ "Snapshot deleted.": "스냅샷이 삭제되었습니다.",
+ "Connected to encrypted live room": "암호화된 라이브 룸에 연결됨",
"Linking folder": "폴더 연결 중",
"Scanning folder…": "폴더 검색 중…",
"Reading files…": "파일 읽는 중…",
diff --git a/assets/i18n/pl.json b/assets/i18n/pl.json
index bcc339e9..e1e4e22f 100644
--- a/assets/i18n/pl.json
+++ b/assets/i18n/pl.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "Udostępnianie szyfrowane. Zachowaj pełny link w tajemnicy; zawiera klucz odszyfrowywania.",
+ "Delete snapshot": "Usuń migawkę",
+ "Snapshot deleted.": "Migawka usunięta.",
+ "Connected to encrypted live room": "Połączono z szyfrowanym pokojem na żywo",
"Linking folder": "Łączenie folderu",
"Scanning folder…": "Skanowanie folderu…",
"Reading files…": "Odczytywanie plików…",
diff --git a/assets/i18n/pt.json b/assets/i18n/pt.json
index 4d179cfd..360961b1 100644
--- a/assets/i18n/pt.json
+++ b/assets/i18n/pt.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "Partilha encriptada. Mantenha a ligação completa privada; inclui a chave de desencriptação.",
+ "Delete snapshot": "Eliminar instantâneo",
+ "Snapshot deleted.": "Instantâneo eliminado.",
+ "Connected to encrypted live room": "Ligado à sala ao vivo encriptada",
"Linking folder": "Vinculando pasta",
"Scanning folder…": "Verificando pasta…",
"Reading files…": "Lendo arquivos…",
diff --git a/assets/i18n/ru.json b/assets/i18n/ru.json
index 5d88eb1a..5ecf0ab5 100644
--- a/assets/i18n/ru.json
+++ b/assets/i18n/ru.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "Зашифрованный общий доступ. Храните полную ссылку в тайне: она содержит ключ расшифровки.",
+ "Delete snapshot": "Удалить снимок",
+ "Snapshot deleted.": "Снимок удалён.",
+ "Connected to encrypted live room": "Подключено к зашифрованной комнате",
"Linking folder": "Подключение папки",
"Scanning folder…": "Сканирование папки…",
"Reading files…": "Чтение файлов…",
diff --git a/assets/i18n/tr.json b/assets/i18n/tr.json
index 74ad6f2c..29cee545 100644
--- a/assets/i18n/tr.json
+++ b/assets/i18n/tr.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "Şifreli paylaşım. Şifre çözme anahtarını içerdiğinden bağlantının tamamını gizli tutun.",
+ "Delete snapshot": "Anlık görüntüyü sil",
+ "Snapshot deleted.": "Anlık görüntü silindi.",
+ "Connected to encrypted live room": "Şifreli canlı odaya bağlanıldı",
"Linking folder": "Klasör bağlanıyor",
"Scanning folder…": "Klasör taranıyor…",
"Reading files…": "Dosyalar okunuyor…",
diff --git a/assets/i18n/tw.json b/assets/i18n/tw.json
index f7f0ba3f..7d4fca3d 100644
--- a/assets/i18n/tw.json
+++ b/assets/i18n/tw.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "加密分享。請將完整連結保密,其中包含解密金鑰。",
+ "Delete snapshot": "刪除快照",
+ "Snapshot deleted.": "快照已刪除。",
+ "Connected to encrypted live room": "已連線至加密的即時協作房間",
"Linking folder": "正在連結資料夾",
"Scanning folder…": "正在掃描資料夾…",
"Reading files…": "正在讀取檔案…",
diff --git a/assets/i18n/uk.json b/assets/i18n/uk.json
index f242705d..d48d1376 100644
--- a/assets/i18n/uk.json
+++ b/assets/i18n/uk.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "Зашифрований спільний доступ. Зберігайте повне посилання в таємниці: воно містить ключ розшифрування.",
+ "Delete snapshot": "Видалити знімок",
+ "Snapshot deleted.": "Знімок видалено.",
+ "Connected to encrypted live room": "Підключено до зашифрованої кімнати",
"Linking folder": "Підключення папки",
"Scanning folder…": "Сканування папки…",
"Reading files…": "Читання файлів…",
diff --git a/assets/i18n/zh.json b/assets/i18n/zh.json
index 8636d728..c2d07794 100644
--- a/assets/i18n/zh.json
+++ b/assets/i18n/zh.json
@@ -1,4 +1,8 @@
{
+ "Encrypted sharing. Keep the complete link private; it includes the decryption key.": "加密分享。请将完整链接保密,其中包含解密密钥。",
+ "Delete snapshot": "删除快照",
+ "Snapshot deleted.": "快照已删除。",
+ "Connected to encrypted live room": "已连接到加密的实时协作房间",
"Linking folder": "正在链接文件夹",
"Scanning folder…": "正在扫描文件夹…",
"Reading files…": "正在读取文件…",
diff --git a/assets/yjs.LICENSE.txt b/assets/yjs.LICENSE.txt
new file mode 100644
index 00000000..a922cce8
--- /dev/null
+++ b/assets/yjs.LICENSE.txt
@@ -0,0 +1,47 @@
+Yjs
+The MIT License (MIT)
+
+Copyright (c) 2023
+ - Kevin Jahns .
+ - Chair of Computer Science 5 (Databases & Information Systems), RWTH Aachen University, Germany
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+lib0
+The MIT License (MIT)
+
+Copyright (c) 2019 Kevin Jahns .
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/assets/yjs.mjs b/assets/yjs.mjs
new file mode 100644
index 00000000..b58dfe72
--- /dev/null
+++ b/assets/yjs.mjs
@@ -0,0 +1,5 @@
+var A=()=>new Map,qe=e=>{let t=A();return e.forEach((n,r)=>{t.set(r,n)}),t},L=(e,t,n)=>{let r=e.get(t);return r===void 0&&e.set(t,r=n()),r},Jr=(e,t)=>{let n=[];for(let[r,s]of e)n.push(t(s,r));return n},Xr=(e,t)=>{for(let[n,r]of e)if(t(r,n))return!0;return!1};var rt=()=>new Set;var Ge=e=>e[e.length-1];var Pr=(e,t)=>{for(let n=0;n{for(let n=0;n{for(let n=0;n{let n=new Array(e);for(let r=0;r{this.off(t,r),n(...s)};this.on(t,r)}off(t,n){let r=this._observers.get(t);r!==void 0&&(r.delete(n),r.size===0&&this._observers.delete(t))}emit(t,n){return Y((this._observers.get(t)||A()).values()).forEach(r=>r(...n))}destroy(){this._observers=A()}};var B=Math.floor;var yt=Math.abs;var He=(e,t)=>ee>t?e:t,kl=Number.isNaN;var Je=e=>e!==0?e<0:1/e<0;var Tt=Number.MAX_SAFE_INTEGER,Ln=Number.MIN_SAFE_INTEGER,Sl=1<<31;var Yr=Number.isInteger||(e=>typeof e=="number"&&isFinite(e)&&B(e)===e),_l=Number.isNaN,El=Number.parseInt;var Mn=String.fromCharCode,$i=String.fromCodePoint,Ul=Mn(65535),Fi=e=>e.toLowerCase(),ji=/^\s*/g,qi=e=>e.replace(ji,""),Gi=/([A-Z])/g,Rn=(e,t)=>qi(e.replace(Gi,n=>`${t}${Fi(n)}`));var zi=e=>{let t=unescape(encodeURIComponent(e)),n=t.length,r=new Uint8Array(n);for(let s=0;sOt.encode(e),Wr=Ot?Hi:zi;var vt=typeof TextDecoder>"u"?null:new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0});vt&&vt.decode(new Uint8Array).length===1&&(vt=null);var Xe=(e,t)=>Kr(t,()=>e).join("");var xt=class{constructor(){this.cpos=0,this.cbuf=new Uint8Array(100),this.bufs=[]}},kt=()=>new xt;var Ji=e=>{let t=e.cpos;for(let n=0;n{let t=new Uint8Array(Ji(e)),n=0;for(let r=0;r{let n=e.cbuf.length;n-e.cpos{let n=e.cbuf.length;e.cpos===n&&(e.bufs.push(e.cbuf),e.cbuf=new Uint8Array(n*2),e.cpos=0),e.cbuf[e.cpos++]=t};var Bt=D;var g=(e,t)=>{for(;t>127;)D(e,128|127&t),t=B(t/128);D(e,127&t)},ge=(e,t)=>{let n=Je(t);for(n&&(t=-t),D(e,(t>63?128:0)|(n?64:0)|63&t),t=B(t/64);t>0;)D(e,(t>127?128:0)|127&t),t=B(t/128)},$n=new Uint8Array(3e4),Pi=$n.length/3,Ki=(e,t)=>{if(t.length{let n=unescape(encodeURIComponent(t)),r=n.length;g(e,r);for(let s=0;sMt(e,q(t)),Mt=(e,t)=>{let n=e.cbuf.length,r=e.cpos,s=He(n-r,t.length),i=t.length-s;e.cbuf.set(t.subarray(0,s),r),e.cpos+=s,i>0&&(e.bufs.push(e.cbuf),e.cbuf=new Uint8Array(it(n*2,i)),e.cbuf.set(t.subarray(s)),e.cpos=i)},$=(e,t)=>{g(e,t.byteLength),Mt(e,t)},Fn=(e,t)=>{Xi(e,t);let n=new DataView(e.cbuf.buffer,e.cpos,t);return e.cpos+=t,n},Wi=(e,t)=>Fn(e,4).setFloat32(0,t,!1),Zi=(e,t)=>Fn(e,8).setFloat64(0,t,!1),Qi=(e,t)=>Fn(e,8).setBigInt64(0,t,!1);var Qr=new DataView(new ArrayBuffer(4)),to=e=>(Qr.setFloat32(0,e),Qr.getFloat32(0)===e),Nt=(e,t)=>{switch(typeof t){case"string":D(e,119),lt(e,t);break;case"number":Yr(t)&&yt(t)<=2147483647?(D(e,125),ge(e,t)):to(t)?(D(e,124),Wi(e,t)):(D(e,123),Zi(e,t));break;case"bigint":D(e,122),Qi(e,t);break;case"object":if(t===null)D(e,126);else if(st(t)){D(e,117),g(e,t.length);for(let n=0;n0&&g(this,this.count-1),this.count=1,this.w(this,t),this.s=t)}};var ts=e=>{e.count>0&&(ge(e.encoder,e.count===1?e.s:-e.s),e.count>1&&g(e.encoder,e.count-2))},bt=class{constructor(){this.encoder=new xt,this.s=0,this.count=0}write(t){this.s===t?this.count++:(ts(this),this.count=1,this.s=t)}toUint8Array(){return ts(this),q(this.encoder)}};var es=e=>{if(e.count>0){let t=e.diff*2+(e.count===1?0:1);ge(e.encoder,t),e.count>1&&g(e.encoder,e.count-2)}},Lt=class{constructor(){this.encoder=new xt,this.s=0,this.count=0,this.diff=0}write(t){this.diff===t-this.s?(this.s=t,this.count++):(es(this),this.count=1,this.diff=t-this.s,this.s=t)}toUint8Array(){return es(this),q(this.encoder)}},Pe=class{constructor(){this.sarr=[],this.s="",this.lensE=new bt}write(t){this.s+=t,this.s.length>19&&(this.sarr.push(this.s),this.s=""),this.lensE.write(t.length)}toUint8Array(){let t=new xt;return this.sarr.push(this.s),this.s="",lt(t,this.sarr.join("")),Mt(t,this.lensE.toUint8Array()),q(t)}};var G=e=>new Error(e),z=()=>{throw G("Method unimplemented")},T=()=>{throw G("Unexpected case")};var Ye=G("Unexpected end of array"),rs=G("Integer out of Range"),no=(e,t,n)=>{if(t<0||n<0||t+n>e.length)throw Ye;return new Uint8Array(e.buffer,e.byteOffset+t,n)},ro=(e,t,n)=>{if(t<0||n<0||t+n>e.length)throw Ye;return new DataView(e.buffer,e.byteOffset+t,n)},Rt=class{constructor(t){this.arr=t,this.pos=0}},v=e=>new Rt(e),qn=e=>e.pos!==e.arr.length;var so=(e,t)=>{let n=no(e.arr,e.pos,t);return e.pos+=t,n},F=e=>so(e,w(e));var St=e=>e.arr[e.pos++];var w=e=>{let t=0,n=1,r=e.arr.length;for(;e.posTt)throw rs}throw Ye},we=e=>{let t=e.arr[e.pos++],n=t&63,r=64,s=(t&64)>0?-1:1;if((t&128)===0)return s*n;let i=e.arr.length;for(;e.posTt)throw rs}throw Ye};var io=e=>{let t=w(e);if(t===0)return"";{let n=String.fromCodePoint(St(e));if(--t<100)for(;t--;)n+=String.fromCodePoint(St(e));else for(;t>0;){let r=t<1e4?t:1e4,s=e.arr.subarray(e.pos,e.pos+r);e.pos+=r,n+=String.fromCodePoint.apply(null,s),t-=r}return decodeURIComponent(escape(n))}},oo=e=>vt.decode(F(e)),ht=vt?oo:io;var Gn=(e,t)=>{let n=ro(e.arr,e.pos,t);return e.pos+=t,n},co=e=>Gn(e,4).getFloat32(0,!1),lo=e=>Gn(e,8).getFloat64(0,!1),ao=e=>Gn(e,8).getBigInt64(0,!1);var ho=[e=>{},e=>null,we,co,lo,ao,e=>!1,e=>!0,ht,e=>{let t=w(e),n={};for(let r=0;r{let t=w(e),n=[];for(let r=0;rho[127-St(e)](e),me=class extends Rt{constructor(t,n){super(t),this.reader=n,this.s=null,this.count=0}read(){return this.count===0&&(this.s=this.reader(this),qn(this)?this.count=w(this)+1:this.count=-1),this.count--,this.s}};var _t=class extends Rt{constructor(t){super(t),this.s=0,this.count=0}read(){if(this.count===0){this.s=we(this);let t=Je(this.s);this.count=1,t&&(this.s=-this.s,this.count=w(this)+2)}return this.count--,this.s}};var Ft=class extends Rt{constructor(t){super(t),this.s=0,this.count=0,this.diff=0}read(){if(this.count===0){let t=we(this),n=t&1;this.diff=B(t/2),this.count=1,n&&(this.count=w(this)+2)}return this.s+=this.diff,this.count--,this.s}},Ke=class{constructor(t){this.decoder=new _t(t),this.str=ht(this.decoder),this.spos=0}read(){let t=this.spos+this.decoder.read(),n=this.str.slice(this.spos,t);return this.spos=t,n}};var Il=crypto.subtle,ss=crypto.getRandomValues.bind(crypto);var zn=()=>ss(new Uint32Array(1))[0];var fo="10000000-1000-4000-8000"+-1e11,is=()=>fo.replace(/[018]/g,e=>(e^zn()&15>>e/4).toString(16));var We=Date.now;var Hn=e=>new Promise(e);var vl=Promise.all.bind(Promise);var Jn=e=>e===void 0?null:e;var Xn=class{constructor(){this.map=new Map}setItem(t,n){this.map.set(t,n)}getItem(t){return this.map.get(t)}},cs=new Xn,wo=!0;try{typeof window<"u"&&typeof localStorage<"u"&&localStorage&&(cs=localStorage,wo=!1)}catch{}var ls=cs;var Et=Symbol("Equality"),Ze=(e,t)=>e===t||!!e?.[Et]?.(t)||!1;var as=e=>typeof e=="object",hs=Object.assign,xo=Object.keys;var us=(e,t)=>{for(let n in e)t(e[n],n)};var ye=e=>xo(e).length;var ds=e=>{for(let t in e)return!1;return!0},jt=(e,t)=>{for(let n in e)if(!t(e[n],n))return!1;return!0},xe=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),Kn=(e,t)=>e===t||ye(e)===ye(t)&&jt(e,(n,r)=>(n!==void 0||xe(t,r))&&Ze(t[r],n)),bo=Object.freeze,Yn=e=>{for(let t in e){let n=e[t];(typeof n=="object"||typeof n=="function")&&Yn(e[t])}return bo(e)};var ke=(e,t,n=0)=>{try{for(;ne;var be=(e,t)=>{if(e===t)return!0;if(e==null||t==null||e.constructor!==t.constructor&&(e.constructor||Object)!==(t.constructor||Object))return!1;if(e[Et]!=null)return e[Et](t);switch(e.constructor){case ArrayBuffer:e=new Uint8Array(e),t=new Uint8Array(t);case Uint8Array:{if(e.byteLength!==t.byteLength)return!1;for(let n=0;nt.includes(e);var Se=typeof process<"u"&&process.release&&/node|io\.js/.test(process.release.name)&&Object.prototype.toString.call(typeof process<"u"?process:0)==="[object process]";var Ol=typeof navigator<"u"?/Mac/.test(navigator.platform):!1,W,ko=[],So=()=>{if(W===void 0)if(Se){W=A();let e=process.argv,t=null;for(let n=0;n{if(e.length!==0){let[t,n]=e.split("=");W.set(`--${Rn(t,"-")}`,n),W.set(`-${Rn(t,"-")}`,n)}})):W=A();return W},tr=e=>So().has(e);var _e=e=>Se?Jn(process.env[e.toUpperCase().replaceAll("-","_")]):Jn(ls.getItem(e));var ps=e=>tr("--"+e)||_e(e)!==null,gs=ps("production"),_o=Se&&fs(process.env.FORCE_COLOR,["true","1","2"]),ms=_o||!tr("--no-colors")&&!ps("no-color")&&(!Se||process.stdout.isTTY)&&(!Se||tr("--color")||_e("COLORTERM")!==null||(_e("TERM")||"").includes("color"));var Eo=e=>new Uint8Array(e);var ws=e=>{let t=Eo(e.byteLength);return t.set(e),t};var nr=class{constructor(t,n){this.left=t,this.right=n}},Z=(e,t)=>new nr(e,t);var rr=e=>e.next()>=.5,Qe=(e,t,n)=>B(e.next()*(n+1-t)+t);var sr=(e,t,n)=>B(e.next()*(n+1-t)+t);var ir=(e,t,n)=>sr(e,t,n);var Ao=e=>Mn(ir(e,97,122)),ys=(e,t=0,n=20)=>{let r=ir(e,t,n),s="";for(let i=0;it[ir(e,0,t.length-1)];var Do=Symbol("0schema"),or=class{constructor(){this._rerrs=[]}extend(t,n,r,s=null){this._rerrs.push({path:t,expected:n,has:r,message:s})}toString(){let t=[];for(let n=this._rerrs.length-1;n>0;n--){let r=this._rerrs[n];t.push(Xe(" ",(this._rerrs.length-n)*2)+`${r.path!=null?`[${r.path}] `:""}${r.has} doesn't match ${r.expected}. ${r.message}`)}return t.join(`
+`)}},cr=(e,t)=>e===t?!0:e==null||t==null||e.constructor!==t.constructor?!1:e[Et]?Ze(e,t):st(e)?ze(e,n=>he(t,r=>cr(n,r))):as(e)?jt(e,(n,r)=>cr(n,t[r])):!1,O=class{static _dilutes=!1;extends(t){let[n,r]=[this.shape,t.shape];return this.constructor._dilutes&&([r,n]=[n,r]),cr(n,r)}equals(t){return this.constructor===t.constructor&&be(this.shape,t.shape)}[Do](){return!0}[Et](t){return this.equals(t)}validate(t){return this.check(t)}check(t,n){z()}get nullable(){return Jt(this,fn)}get optional(){return new en(this)}cast(t){return xs(t,this),t}expect(t){return xs(t,this),t}},Ee=class extends O{constructor(t,n){super(),this.shape=t,this._c=n}check(t,n=void 0){let r=t?.constructor===this.shape&&(this._c==null||this._c(t));return!r&&n?.extend(null,this.shape.name,t?.constructor.name,t?.constructor!==this.shape?"Constructor match failed":"Check failed"),r}},E=(e,t=null)=>new Ee(e,t),Nl=E(Ee),Ue=class extends O{constructor(t){super(),this.shape=t}check(t,n){let r=this.shape(t);return!r&&n?.extend(null,"custom prop",t?.constructor.name,"failed to check custom prop"),r}},I=e=>new Ue(e),Ll=E(Ue),zt=class extends O{constructor(t){super(),this.shape=t}check(t,n){let r=this.shape.some(s=>s===t);return!r&&n?.extend(null,this.shape.join(" | "),t.toString()),r}},un=(...e)=>new zt(e),bs=E(zt),To=RegExp.escape||(e=>e.replace(/[().|&,$^[\]]/g,t=>"\\"+t)),ks=e=>{if(Ht.check(e))return[To(e)];if(bs.check(e))return e.shape.map(t=>t+"");if(Cs.check(e))return["[+-]?\\d+.?\\d*"];if(As.check(e))return[".*"];if(an.check(e))return e.shape.map(ks).flat(1);T()},lr=class extends O{constructor(t){super(),this.shape=t,this._r=new RegExp("^"+t.map(ks).map(n=>`(${n.join("|")})`).join("")+"$")}check(t,n){let r=this._r.exec(t)!=null;return!r&&n?.extend(null,this._r.toString(),t.toString(),"String doesn't match string template."),r}};var Bl=E(lr),vo=Symbol("optional"),en=class extends O{constructor(t){super(),this.shape=t}check(t,n){let r=t===void 0||this.shape.check(t);return!r&&n?.extend(null,"undefined (optional)","()"),r}get[vo](){return!0}},Oo=E(en),nn=class extends O{check(t,n){return n?.extend(null,"never",typeof t),!1}},Ml=new nn,Rl=E(nn),rn=class e extends O{constructor(t,n=!1){super(),this.shape=t,this._isPartial=n}static _dilutes=!0;get partial(){return new e(this.shape,!0)}check(t,n){return t==null?(n?.extend(null,"object","null"),!1):jt(this.shape,(r,s)=>{let i=this._isPartial&&!xe(t,s)||r.check(t[s],n);return!i&&n?.extend(s.toString(),r.toString(),typeof t[s],"Object property does not match"),i})}},Vo=e=>new rn(e),No=E(rn),Lo=I(e=>e!=null&&(e.constructor===Object||e.constructor==null)),sn=class extends O{constructor(t,n){super(),this.shape={keys:t,values:n}}check(t,n){return t!=null&&jt(t,(r,s)=>{let i=this.shape.keys.check(s,n);return!i&&n?.extend(s+"","Record",typeof t,i?"Key doesn't match schema":"Value doesn't match value"),i&&this.shape.values.check(r,n)})}},Ss=(e,t)=>new sn(e,t),Bo=E(sn),on=class extends O{constructor(t){super(),this.shape=t}check(t,n){return t!=null&&jt(this.shape,(r,s)=>{let i=r.check(t[s],n);return!i&&n?.extend(s.toString(),"Tuple",typeof r),i})}},Mo=(...e)=>new on(e),$l=E(on),cn=class extends O{constructor(t){super(),this.shape=t.length===1?t[0]:new Ce(t)}check(t,n){let r=st(t)&&ze(t,s=>this.shape.check(s));return!r&&n?.extend(null,"Array",""),r}},_s=(...e)=>new cn(e),Ro=E(cn),$o=I(e=>st(e)),ln=class extends O{constructor(t,n){super(),this.shape=t,this._c=n}check(t,n){let r=t instanceof this.shape&&(this._c==null||this._c(t));return!r&&n?.extend(null,this.shape.name,t?.constructor.name),r}},Fo=(e,t=null)=>new ln(e,t),Fl=E(ln),jo=Fo(O),ar=class extends O{constructor(t){super(),this.len=t.length-1,this.args=Mo(...t.slice(-1)),this.res=t[this.len]}check(t,n){let r=t.constructor===Function&&t.length<=this.len;return!r&&n?.extend(null,"function",typeof t),r}};var qo=E(ar),Go=I(e=>typeof e=="function"),hr=class extends O{constructor(t){super(),this.shape=t}check(t,n){let r=ze(this.shape,s=>s.check(t,n));return!r&&n?.extend(null,"Intersectinon",typeof t),r}};var jl=E(hr,e=>e.shape.length>0),Ce=class extends O{static _dilutes=!0;constructor(t){super(),this.shape=t}check(t,n){let r=he(this.shape,s=>s.check(t,n));return n?.extend(null,"Union",typeof t),r}},Jt=(...e)=>e.findIndex(t=>an.check(t))>=0?Jt(...e.map(t=>Ae(t)).map(t=>an.check(t)?t.shape:[t]).flat(1)):e.length===1?e[0]:new Ce(e),an=E(Ce),Es=()=>!0,hn=I(Es),zo=E(Ue,e=>e.shape===Es),dr=I(e=>typeof e=="bigint"),Ho=I(e=>e===dr),Us=I(e=>typeof e=="symbol"),ql=I(e=>e===Us),Gt=I(e=>typeof e=="number"),Cs=I(e=>e===Gt),Ht=I(e=>typeof e=="string"),As=I(e=>e===Ht),dn=I(e=>typeof e=="boolean"),Jo=I(e=>e===dn),Is=un(void 0),Gl=E(zt,e=>e.shape.length===1&&e.shape[0]===void 0),zl=un(void 0);var fn=un(null),Xo=E(zt,e=>e.shape.length===1&&e.shape[0]===null),Hl=E(Uint8Array),Jl=E(Ee,e=>e.shape===Uint8Array),Po=Jt(Gt,Ht,fn,Is,dr,dn,Us),Xl=(()=>{let e=_s(hn),t=Ss(Ht,hn),n=Jt(Gt,Ht,fn,dn,e,t);return e.shape=n,t.shape.values=n,n})(),Ae=e=>{if(jo.check(e))return e;if(Lo.check(e)){let t={};for(let n in e)t[n]=Ae(e[n]);return Vo(t)}else{if($o.check(e))return Jt(...e.map(Ae));if(Po.check(e))return un(e);if(Go.check(e))return E(e)}T()},xs=gs?()=>{}:(e,t)=>{let n=new or;if(!t.check(e,n))throw G(`Expected value to be of type ${t.constructor.name}.
+${n.toString()}`)},ur=class{constructor(t){this.patterns=[],this.$state=t}if(t,n){return this.patterns.push({if:Ae(t),h:n}),this}else(t){return this.if(hn,t)}done(){return(t,n)=>{for(let r=0;rnew ur(e),Ds=Ko(hn).if(Cs,(e,t)=>Qe(t,Ln,Tt)).if(As,(e,t)=>ys(t)).if(Jo,(e,t)=>rr(t)).if(Ho,(e,t)=>BigInt(Qe(t,Ln,Tt))).if(an,(e,t)=>qt(t,tn(t,e.shape))).if(No,(e,t)=>{let n={};for(let r in e.shape){let s=e.shape[r];if(Oo.check(s)){if(rr(t))continue;s=s.shape}n[r]=Ds(s,t)}return n}).if(Ro,(e,t)=>{let n=[],r=sr(t,0,42);for(let s=0;stn(t,e.shape)).if(Xo,(e,t)=>null).if(qo,(e,t)=>{let n=qt(t,e.res);return()=>n}).if(zo,(e,t)=>qt(t,tn(t,[Gt,Ht,fn,Is,dr,dn,_s(Gt),Ss(Jt("a","b","c"),Gt)]))).if(Bo,(e,t)=>{let n={},r=Qe(t,0,3);for(let s=0;sDs(Ae(t),e);var Ut=typeof document<"u"?document:{};var Pl=I(e=>e.nodeType===tc);var Kl=typeof DOMParser<"u"?new DOMParser:null;var Yl=I(e=>e.nodeType===Wo);var Wl=I(e=>e.nodeType===Zo);var Ts=e=>Jr(e,(t,n)=>`${n}:${t};`).join("");var Wo=Ut.ELEMENT_NODE,Zo=Ut.TEXT_NODE,Zl=Ut.CDATA_SECTION_NODE,Ql=Ut.COMMENT_NODE,Qo=Ut.DOCUMENT_NODE,ta=Ut.DOCUMENT_TYPE_NODE,tc=Ut.DOCUMENT_FRAGMENT_NODE,ea=I(e=>e.nodeType===Qo);var Q=Symbol;var Ie=Q(),De=Q(),fr=Q(),pr=Q(),gr=Q(),Te=Q(),mr=Q(),Xt=Q(),wr=Q(),vs=e=>{e.length===1&&e[0]?.constructor===Function&&(e=e[0]());let t=[],n=[],r=0;for(;r0&&n.push(t.join(""));r{e.length===1&&e[0]?.constructor===Function&&(e=e[0]());let t=[],n=[],r=A(),s=[],i=0;for(;i0||l.length>0?(t.push("%c"+o),n.push(l)):t.push(o)}else break}}for(i>0&&(s=n,s.unshift(t.join("")));i{console.log(...Os(e)),Vs.forEach(t=>t.print(e))},yr=(...e)=>{console.warn(...Os(e)),e.unshift(Xt),Vs.forEach(t=>t.print(e))};var Vs=rt();var Ns=e=>({[Symbol.iterator](){return this},next:e}),Ls=(e,t)=>Ns(()=>{let n;do n=e.next();while(!n.done&&!t(n.value));return n}),gn=(e,t)=>Ns(()=>{let{done:n,value:r}=e.next();return{done:n,value:n?void 0:t(r)}});var Rs=class extends Dt{constructor(t,n){super(),this.doc=t,this.awareness=n}},Yt=class{constructor(t,n){this.clock=t,this.len=n}},gt=class{constructor(){this.clients=new Map}},Wt=(e,t,n)=>t.clients.forEach((r,s)=>{let i=e.doc.store.clients.get(s);if(i!=null){let o=i[i.length-1],c=o.id.clock+o.length;for(let l=0,h=r[l];l{let n=0,r=e.length-1;for(;n<=r;){let s=B((n+r)/2),i=e[s],o=i.clock;if(o<=t){if(t{let n=e.clients.get(t.client);return n!==void 0&&hc(n,t.clock)!==null},Nr=e=>{e.clients.forEach(t=>{t.sort((s,i)=>s.clock-i.clock);let n,r;for(n=1,r=1;n=i.clock?t[r-1]=new Yt(s.clock,it(s.len,i.clock+i.len-s.clock)):(r{let t=new gt;for(let n=0;n{if(!t.clients.has(s)){let i=r.slice();for(let o=n+1;o{L(e.clients,t,()=>[]).push(new Yt(n,r))},Lr=()=>new gt,ri=e=>{let t=Lr();return e.clients.forEach((n,r)=>{let s=[];for(let i=0;i0&&t.clients.set(r,s)}),t},tt=(e,t)=>{g(e.restEncoder,t.clients.size),Y(t.clients.entries()).sort((n,r)=>r[0]-n[0]).forEach(([n,r])=>{e.resetDsCurVal(),g(e.restEncoder,n);let s=r.length;g(e.restEncoder,s);for(let i=0;i{let t=new gt,n=w(e.restDecoder);for(let r=0;r0){let o=L(t.clients,s,()=>[]);for(let c=0;c{let r=new gt,s=w(e.restDecoder);for(let i=0;i0){let i=new X;return g(i.restEncoder,0),tt(i,r),i.toUint8Array()}return null},uc=(e,t)=>{if(e.clients.size!==t.clients.size)return!1;for(let[n,r]of e.clients.entries()){let s=t.clients.get(n);if(s===void 0||r.length!==s.length)return!1;for(let i=0;i!0,meta:i=null,autoLoad:o=!1,shouldLoad:c=!0}={}){super(),this.gc=r,this.gcFilter=s,this.clientID=si(),this.guid=t,this.collectionid=n,this.share=new Map,this.store=new En,this._transaction=null,this._transactionCleanups=[],this.subdocs=new Set,this._item=null,this.shouldLoad=c,this.autoLoad=o,this.meta=i,this.isLoaded=!1,this.isSynced=!1,this.isDestroyed=!1,this.whenLoaded=Hn(h=>{this.on("load",()=>{this.isLoaded=!0,h(this)})});let l=()=>Hn(h=>{let a=u=>{(u===void 0||u===!0)&&(this.off("sync",a),h())};this.on("sync",a)});this.on("sync",h=>{h===!1&&this.isSynced&&(this.whenSynced=l()),this.isSynced=h===void 0||h===!0,this.isSynced&&!this.isLoaded&&this.emit("load",[this])}),this.whenSynced=l()}load(){let t=this._item;t!==null&&!this.shouldLoad&&x(t.parent.doc,n=>{n.subdocsLoaded.add(this)},null,!0),this.shouldLoad=!0}getSubdocs(){return this.subdocs}getSubdocGuids(){return new Set(Y(this.subdocs).map(t=>t.guid))}transact(t,n=null){return x(this,t,n)}get(t,n=U){let r=L(this.share,t,()=>{let i=new n;return i._integrate(this,null),i}),s=r.constructor;if(n!==U&&s!==n)if(s===U){let i=new n;i._map=r._map,r._map.forEach(o=>{for(;o!==null;o=o.left)o.parent=i}),i._start=r._start;for(let o=i._start;o!==null;o=o.right)o.parent=i;return i._length=r._length,this.share.set(t,i),i._integrate(this,null),i}else throw new Error(`Type with the name ${t} has already been defined with a different constructor`);return r}getArray(t=""){return this.get(t,ee)}getText(t=""){return this.get(t,Re)}getMap(t=""){return this.get(t,ne)}getXmlElement(t=""){return this.get(t,se)}getXmlFragment(t=""){return this.get(t,re)}toJSON(){let t={};return this.share.forEach((n,r)=>{t[r]=n.toJSON()}),t}destroy(){this.isDestroyed=!0,Y(this.subdocs).forEach(n=>n.destroy());let t=this._item;if(t!==null){this._item=null;let n=t.content;n.doc=new e({guid:this.guid,...n.opts,shouldLoad:!1}),n.doc._item=t,x(t.parent.doc,r=>{let s=n.doc;t.deleted||r.subdocsAdded.add(s),r.subdocsRemoved.add(this)},null,!0)}this.emit("destroyed",[!0]),this.emit("destroy",[this]),super.destroy()}},At=class{constructor(t){this.restDecoder=t}resetDsCurVal(){}readDsClock(){return w(this.restDecoder)}readDsLen(){return w(this.restDecoder)}},H=class extends At{readLeftID(){return m(w(this.restDecoder),w(this.restDecoder))}readRightID(){return m(w(this.restDecoder),w(this.restDecoder))}readClient(){return w(this.restDecoder)}readInfo(){return St(this.restDecoder)}readString(){return ht(this.restDecoder)}readParentInfo(){return w(this.restDecoder)===1}readTypeRef(){return w(this.restDecoder)}readLen(){return w(this.restDecoder)}readAny(){return $t(this.restDecoder)}readBuf(){return ws(F(this.restDecoder))}readJSON(){return JSON.parse(ht(this.restDecoder))}readKey(){return ht(this.restDecoder)}},kn=class{constructor(t){this.dsCurrVal=0,this.restDecoder=t}resetDsCurVal(){this.dsCurrVal=0}readDsClock(){return this.dsCurrVal+=w(this.restDecoder),this.dsCurrVal}readDsLen(){let t=w(this.restDecoder)+1;return this.dsCurrVal+=t,t}},j=class extends kn{constructor(t){super(t),this.keys=[],w(t),this.keyClockDecoder=new Ft(F(t)),this.clientDecoder=new _t(F(t)),this.leftClockDecoder=new Ft(F(t)),this.rightClockDecoder=new Ft(F(t)),this.infoDecoder=new me(F(t),St),this.stringDecoder=new Ke(F(t)),this.parentInfoDecoder=new me(F(t),St),this.typeRefDecoder=new _t(F(t)),this.lenDecoder=new _t(F(t))}readLeftID(){return new pt(this.clientDecoder.read(),this.leftClockDecoder.read())}readRightID(){return new pt(this.clientDecoder.read(),this.rightClockDecoder.read())}readClient(){return this.clientDecoder.read()}readInfo(){return this.infoDecoder.read()}readString(){return this.stringDecoder.read()}readParentInfo(){return this.parentInfoDecoder.read()===1}readTypeRef(){return this.typeRefDecoder.read()}readLen(){return this.lenDecoder.read()}readAny(){return $t(this.restDecoder)}readBuf(){return F(this.restDecoder)}readJSON(){return $t(this.restDecoder)}readKey(){let t=this.keyClockDecoder.read();if(t{r=it(r,t[0].id.clock);let s=P(t,r);g(e.restEncoder,t.length-s),e.writeClient(n),g(e.restEncoder,r);let i=t[s];i.write(e,r-i.id.clock);for(let o=s+1;o{let r=new Map;n.forEach((s,i)=>{_(t,i)>s&&r.set(i,s)}),je(t).forEach((s,i)=>{n.has(i)||r.set(i,0)}),g(e.restEncoder,r.size),Y(r.entries()).sort((s,i)=>i[0]-s[0]).forEach(([s,i])=>{dc(e,t.clients.get(s),s,i)})},fc=(e,t)=>{let n=A(),r=w(e.restDecoder);for(let s=0;s{let r=[],s=Y(n.keys()).sort((f,p)=>f-p);if(s.length===0)return null;let i=()=>{if(s.length===0)return null;let f=n.get(s[s.length-1]);for(;f.refs.length===f.i;)if(s.pop(),s.length>0)f=n.get(s[s.length-1]);else return null;return f},o=i();if(o===null)return null;let c=new En,l=new Map,h=(f,p)=>{let y=l.get(f);(y==null||y>p)&&l.set(f,p)},a=o.refs[o.i++],u=new Map,d=()=>{for(let f of r){let p=f.id.client,y=n.get(p);y?(y.i--,c.clients.set(p,y.refs.slice(y.i)),n.delete(p),y.i=0,y.refs=[]):c.clients.set(p,[f]),s=s.filter(k=>k!==p)}r.length=0};for(;;){if(a.constructor!==N){let p=L(u,a.id.client,()=>_(t,a.id.client))-a.id.clock;if(p<0)r.push(a),h(a.id.client,a.id.clock-1),d();else{let y=a.getMissing(e,t);if(y!==null){r.push(a);let k=n.get(y)||{refs:[],i:0};if(k.refs.length===k.i)h(y,_(t,y)),d();else{a=k.refs[k.i++];continue}}else(p===0||p0)a=r.pop();else if(o!==null&&o.i0){let f=new X;return Br(f,c,new Map),g(f.restEncoder,0),{missing:l,update:f.toUint8Array()}}return null},gc=(e,t)=>Br(e,t.doc.store,t.beforeState),ii=(e,t,n,r=new j(e))=>x(t,s=>{s.local=!1;let i=!1,o=s.doc,c=o.store,l=fc(r,o),h=pc(s,c,l),a=c.pendingStructs;if(a){for(let[d,f]of a.missing)if(f<_(c,d)){i=!0;break}if(h){for(let[d,f]of h.missing){let p=a.missing.get(d);(p==null||p>f)&&a.missing.set(d,f)}a.update=Un([a.update,h.update])}}else c.pendingStructs=h;let u=$s(r,s,c);if(c.pendingDs){let d=new j(v(c.pendingDs));w(d.restDecoder);let f=$s(d,s,c);u&&f?c.pendingDs=Un([u,f]):c.pendingDs=u||f}else c.pendingDs=u;if(i){let d=c.pendingStructs.update;c.pendingStructs=null,Mr(s.doc,d)}},n,!1),da=(e,t,n)=>ii(e,t,n,new H(e)),Mr=(e,t,n,r=j)=>{let s=v(t);ii(s,e,n,new r(s))},fa=(e,t,n)=>Mr(e,t,n,H),mc=(e,t,n=new Map)=>{Br(e,t.store,n),tt(e,ri(t.store))},wc=(e,t=new Uint8Array([0]),n=new X)=>{let r=ci(t);mc(n,e,r);let s=[n.toUint8Array()];if(e.store.pendingDs&&s.push(e.store.pendingDs),e.store.pendingStructs&&s.push(mi(e.store.pendingStructs.update,t)),s.length>1){if(n.constructor===ct)return Oc(s.map((i,o)=>o===0?i:Bc(i)));if(n.constructor===X)return Un(s)}return s[0]},pa=(e,t)=>wc(e,t,new ct),oi=e=>{let t=new Map,n=w(e.restDecoder);for(let r=0;roi(new At(v(e))),Rr=(e,t)=>(g(e.restEncoder,t.size),Y(t.entries()).sort((n,r)=>r[0]-n[0]).forEach(([n,r])=>{g(e.restEncoder,n),g(e.restEncoder,r)}),e),yc=(e,t)=>Rr(e,je(t.store)),xc=(e,t=new Qt)=>(e instanceof Map?Rr(t,e):yc(t,e),t.toUint8Array()),ga=e=>xc(e,new mt),br=class{constructor(){this.l=[]}},Fs=()=>new br,js=(e,t)=>e.l.push(t),qs=(e,t)=>{let n=e.l,r=n.length;e.l=n.filter(s=>t!==s),r===e.l.length&&console.error("[yjs] Tried to remove event handler that doesn't exist.")},li=(e,t,n)=>ke(e.l,[t,n]),pt=class{constructor(t,n){this.client=t,this.clock=n}},Pt=(e,t)=>e===t||e!==null&&t!==null&&e.client===t.client&&e.clock===t.clock,m=(e,t)=>new pt(e,t),Gs=(e,t)=>{g(e,t.client),g(e,t.clock)},zs=e=>m(w(e),w(e)),ai=e=>{for(let[t,n]of e.doc.share.entries())if(n===e)return t;throw T()},Sn=(e,t)=>{for(;t!==null;){if(t.parent===e)return!0;t=t.parent._item}return!1},ma=e=>{let t=[],n=e._start;for(;n;)t.push(n),n=n.right;console.log("Children: ",t),console.log("Children content: ",t.filter(r=>!r.deleted).map(r=>r.content))},Hs=class{constructor(t,n=t.getMap("users")){let r=new Map;this.yusers=n,this.doc=t,this.clients=new Map,this.dss=r;let s=(i,o)=>{let c=i.get("ds"),l=i.get("ids"),h=a=>this.clients.set(a,o);c.observe(a=>{a.changes.added.forEach(u=>{u.content.getContent().forEach(d=>{d instanceof Uint8Array&&this.dss.set(o,Zt([this.dss.get(o)||Lr(),ot(new At(v(d)))]))})})}),this.dss.set(o,Zt(c.map(a=>ot(new At(v(a)))))),l.observe(a=>a.changes.added.forEach(u=>u.content.getContent().forEach(h))),l.forEach(h)};n.observe(i=>{i.keysChanged.forEach(o=>s(n.get(o),o))}),n.forEach(s)}setUserMapping(t,n,r,{filter:s=()=>!0}={}){let i=this.yusers,o=i.get(r);o||(o=new ne,o.set("ids",new ee),o.set("ds",new ee),i.set(r,o)),o.get("ids").push([n]),i.observe(c=>{setTimeout(()=>{let l=i.get(r);if(l!==o){o=l,this.clients.forEach((u,d)=>{r===u&&o.get("ids").push([d])});let h=new mt,a=this.dss.get(r);a&&(tt(h,a),o.get("ds").push([h.toUint8Array()]))}},0)}),t.on("afterTransaction",c=>{setTimeout(()=>{let l=o.get("ds"),h=c.deleteSet;if(c.local&&h.clients.size>0&&s(c,h)){let a=new mt;tt(a,h),l.push([a.toUint8Array()])}})})}getUserByClientId(t){return this.clients.get(t)||null}getUserByDeletedId(t){for(let[n,r]of this.dss.entries())if(ce(r,t))return n;return null}},Ve=class{constructor(t,n,r,s=0){this.type=t,this.tname=n,this.item=r,this.assoc=s}},wa=e=>{let t={};return e.type&&(t.type=e.type),e.tname&&(t.tname=e.tname),e.item&&(t.item=e.item),e.assoc!=null&&(t.assoc=e.assoc),t},ya=e=>new Ve(e.type==null?null:m(e.type.client,e.type.clock),e.tname??null,e.item==null?null:m(e.item.client,e.item.clock),e.assoc==null?0:e.assoc),kr=class{constructor(t,n,r=0){this.type=t,this.index=n,this.assoc=r}},bc=(e,t,n=0)=>new kr(e,t,n),mn=(e,t,n)=>{let r=null,s=null;return e._item===null?s=ai(e):r=m(e._item.id.client,e._item.id.clock),new Ve(r,s,t,n)},xa=(e,t,n=0)=>{let r=e._start;if(n<0){if(t===0)return mn(e,null,n);t--}for(;r!==null;){if(!r.deleted&&r.countable){if(r.length>t)return mn(e,m(r.id.client,r.id.clock+t),n);t-=r.length}if(r.right===null&&n<0)return mn(e,r.lastId,n);r=r.right}return mn(e,null,n)},kc=(e,t)=>{let{type:n,tname:r,item:s,assoc:i}=t;if(s!==null)g(e,0),Gs(e,s);else if(r!==null)Bt(e,1),lt(e,r);else if(n!==null)Bt(e,2),Gs(e,n);else throw T();return ge(e,i),e},ba=e=>{let t=kt();return kc(t,e),q(t)},Sc=e=>{let t=null,n=null,r=null;switch(w(e)){case 0:r=zs(e);break;case 1:n=ht(e);break;case 2:t=zs(e)}let s=qn(e)?we(e):0;return new Ve(t,n,r,s)},ka=e=>Sc(v(e)),_c=(e,t)=>{let n=Kt(e,t),r=t.clock-n.id.clock;return{item:n,diff:r}},Sa=(e,t,n=!0)=>{let r=t.store,s=e.item,i=e.type,o=e.tname,c=e.assoc,l=null,h=0;if(s!==null){if(_(r,s.client)<=s.clock)return null;let a=n?Or(r,s):_c(r,s),u=a.item;if(!(u instanceof b))return null;if(l=u.parent,l._item===null||!l._item.deleted){h=u.deleted||!u.countable?0:a.diff+(c>=0?0:1);let d=u.left;for(;d!==null;)!d.deleted&&d.countable&&(h+=d.length),d=d.left}}else{if(o!==null)l=t.get(o);else if(i!==null){if(_(r,i.client)<=i.clock)return null;let{item:a}=n?Or(r,i):{item:Kt(r,i)};if(a instanceof b&&a.content instanceof K)l=a.content.type;else return null}else throw T();c>=0?h=l._length:h=0}return bc(l,h,e.assoc)},_a=(e,t)=>e===t||e!==null&&t!==null&&e.tname===t.tname&&Pt(e.item,t.item)&&Pt(e.type,t.type)&&e.assoc===t.assoc,_n=class{constructor(t,n){this.ds=t,this.sv=n}},Ea=(e,t)=>{let n=e.ds.clients,r=t.ds.clients,s=e.sv,i=t.sv;if(s.size!==i.size||n.size!==r.size)return!1;for(let[o,c]of s.entries())if(i.get(o)!==c)return!1;for(let[o,c]of n.entries()){let l=r.get(o)||[];if(c.length!==l.length)return!1;for(let h=0;h(tt(t,e.ds),Rr(t,e.sv),t.toUint8Array()),Ua=e=>Ec(e,new mt),Uc=(e,t=new kn(v(e)))=>new _n(ot(t),oi(t)),Ca=e=>Uc(e,new At(v(e))),hi=(e,t)=>new _n(e,t),Aa=hi(Lr(),new Map),Ia=e=>hi(ri(e.store),je(e.store)),ut=(e,t)=>t===void 0?!e.deleted:t.sv.has(e.id.client)&&(t.sv.get(e.id.client)||0)>e.id.clock&&!ce(t.ds,e.id),Sr=(e,t)=>{let n=L(e.meta,Sr,rt),r=e.doc.store;n.has(t)||(t.sv.forEach((s,i)=>{s<_(r,i)&&M(e,m(i,s))}),Wt(e,t.ds,s=>{}),n.add(t))},Da=(e,t,n=new Ct)=>{if(e.gc)throw new Error("Garbage-collection must be disabled in `originDoc`!");let{sv:r,ds:s}=t,i=new X;return e.transact(o=>{let c=0;r.forEach(l=>{l>0&&c++}),g(i.restEncoder,c);for(let[l,h]of r){if(h===0)continue;h<_(e.store,l)&&M(o,m(l,h));let a=e.store.clients.get(l)||[],u=P(a,h-1);g(i.restEncoder,u+1),i.writeClient(l),g(i.restEncoder,0);for(let d=0;d<=u;d++)a[d].write(i,0)}tt(i,s)}),Mr(n,i.toUint8Array(),"snapshot"),n},Cc=(e,t,n=j)=>{let r=new n(v(t)),s=new et(r,!1);for(let o=s.curr;o!==null;o=s.next())if((e.sv.get(o.id.client)||0)Cc(e,t,H),En=class{constructor(){this.clients=new Map,this.pendingStructs=null,this.pendingDs=null}},je=e=>{let t=new Map;return e.clients.forEach((n,r)=>{let s=n[n.length-1];t.set(r,s.id.clock+s.length)}),t},_=(e,t)=>{let n=e.clients.get(t);if(n===void 0)return 0;let r=n[n.length-1];return r.id.clock+r.length},ui=(e,t)=>{let n=e.clients.get(t.id.client);if(n===void 0)n=[],e.clients.set(t.id.client,n);else{let r=n[n.length-1];if(r.id.clock+r.length!==t.id.clock)throw T()}n.push(t)},P=(e,t)=>{let n=0,r=e.length-1,s=e[r],i=s.id.clock;if(i===t)return r;let o=B(t/(i+s.length-1)*r);for(;n<=r;){if(s=e[o],i=s.id.clock,i<=t){if(t{let n=e.clients.get(t.client);return n[P(n,t.clock)]},Kt=Ac,_r=(e,t,n)=>{let r=P(t,n),s=t[r];return s.id.clock{let n=e.doc.store.clients.get(t.client);return n[_r(e,n,t.clock)]},Js=(e,t,n)=>{let r=t.clients.get(n.client),s=P(r,n.clock),i=r[s];return n.clock!==i.id.clock+i.length-1&&i.constructor!==R&&r.splice(s+1,0,Tn(e,i,n.clock-i.id.clock+1)),i},Ic=(e,t,n)=>{let r=e.clients.get(t.id.client);r[P(r,t.id.clock)]=n},di=(e,t,n,r,s)=>{if(r===0)return;let i=n+r,o=_r(e,t,n),c;do c=t[o++],it.deleteSet.clients.size===0&&!Xr(t.afterState,(n,r)=>t.beforeState.get(r)!==n)?!1:(Nr(t.deleteSet),gc(e,t),tt(e,t.deleteSet),!0),Ps=(e,t,n)=>{let r=t._item;(r===null||r.id.clock<(e.beforeState.get(r.id.client)||0)&&!r.deleted)&&L(e.changed,t,rt).add(n)},xn=(e,t)=>{let n=e[t],r=e[t-1],s=t;for(;s>0;n=r,r=e[--s-1]){if(r.deleted===n.deleted&&r.constructor===n.constructor&&r.mergeWith(n)){n instanceof b&&n.parentSub!==null&&n.parent._map.get(n.parentSub)===n&&n.parent._map.set(n.parentSub,r);continue}break}let i=t-s;return i&&e.splice(t+1-i,i),i},fi=(e,t,n)=>{for(let[r,s]of e.clients.entries()){let i=t.clients.get(r);for(let o=s.length-1;o>=0;o--){let c=s[o],l=c.clock+c.len;for(let h=P(i,c.clock),a=i[h];h{e.clients.forEach((n,r)=>{let s=t.clients.get(r);for(let i=n.length-1;i>=0;i--){let o=n[i],c=He(s.length-1,1+P(s,o.clock+o.len-1));for(let l=c,h=s[l];l>0&&h.id.clock>=o.clock;h=s[l])l-=1+xn(s,l)}})},va=(e,t,n)=>{fi(e,t,n),pi(e,t)},gi=(e,t)=>{if(tc.push(()=>{(h._item===null||!h._item.deleted)&&h._callObserver(n,l)})),c.push(()=>{n.changedParentTypes.forEach((l,h)=>{h._dEH.l.length>0&&(h._item===null||!h._item.deleted)&&(l=l.filter(a=>a.target._item===null||!a.target._item.deleted),l.forEach(a=>{a.currentTarget=h,a._path=null}),l.sort((a,u)=>a.path.length-u.path.length),c.push(()=>{li(h._dEH,l,n)}))}),c.push(()=>r.emit("afterTransaction",[n,r])),c.push(()=>{n._needFormattingCleanup&&Xc(n)})}),ke(c,[])}finally{r.gc&&fi(i,s,r.gcFilter),pi(i,s),n.afterState.forEach((a,u)=>{let d=n.beforeState.get(u)||0;if(d!==a){let f=s.clients.get(u),p=it(P(f,d),1);for(let y=f.length-1;y>=p;)y-=1+xn(f,y)}});for(let a=o.length-1;a>=0;a--){let{client:u,clock:d}=o[a].id,f=s.clients.get(u),p=P(f,d);p+11||p>0&&xn(f,p)}if(!n.local&&n.afterState.get(r.clientID)!==n.beforeState.get(r.clientID)&&(pn(Xt,Ie,"[yjs] ",De,Te,"Changed the client-id because another client seems to be using it."),r.clientID=si()),r.emit("afterTransactionCleanup",[n,r]),r._observers.has("update")){let a=new ct;Xs(a,n)&&r.emit("update",[a.toUint8Array(),n.origin,r,n])}if(r._observers.has("updateV2")){let a=new X;Xs(a,n)&&r.emit("updateV2",[a.toUint8Array(),n.origin,r,n])}let{subdocsAdded:c,subdocsLoaded:l,subdocsRemoved:h}=n;(c.size>0||h.size>0||l.size>0)&&(c.forEach(a=>{a.clientID=r.clientID,a.collectionid==null&&(a.collectionid=r.collectionid),r.subdocs.add(a)}),h.forEach(a=>r.subdocs.delete(a)),r.emit("subdocs",[{loaded:l,added:c,removed:h},r,n]),h.forEach(a=>a.destroy())),e.length<=t+1?(r._transactionCleanups=[],r.emit("afterAllTransactions",[r,e])):gi(e,t+1)}}},x=(e,t,n=null,r=!0)=>{let s=e._transactionCleanups,i=!1,o=null;e._transaction===null&&(i=!0,e._transaction=new Er(e,n,r),s.push(e._transaction),s.length===1&&e.emit("beforeAllTransactions",[e]),e.emit("beforeTransaction",[e._transaction,e]));try{o=t(e._transaction)}finally{if(i){let c=e._transaction===s[0];e._transaction=null,c&&gi(s,0)}}return o},Ur=class{constructor(t,n){this.insertions=n,this.deletions=t,this.meta=new Map}},Ks=(e,t,n)=>{Wt(e,n.deletions,r=>{r instanceof b&&t.scope.some(s=>s===e.doc||Sn(s,r))&&Gr(r,!1)})},Ys=(e,t,n)=>{let r=null,s=e.doc,i=e.scope;x(s,c=>{for(;t.length>0&&e.currStackItem===null;){let l=s.store,h=t.pop(),a=new Set,u=[],d=!1;Wt(c,h.insertions,f=>{if(f instanceof b){if(f.redone!==null){let{item:p,diff:y}=Or(l,f.id);y>0&&(p=M(c,m(p.id.client,p.id.clock+y))),f=p}!f.deleted&&i.some(p=>p===c.doc||Sn(p,f))&&u.push(f)}}),Wt(c,h.deletions,f=>{f instanceof b&&i.some(p=>p===c.doc||Sn(p,f))&&!ce(h.insertions,f.id)&&a.add(f)}),a.forEach(f=>{d=Li(c,f,a,h.insertions,e.ignoreRemoteMapChanges,e)!==null||d});for(let f=u.length-1;f>=0;f--){let p=u[f];e.deleteFilter(p)&&(p.delete(c),d=!0)}e.currStackItem=d?h:null}c.changed.forEach((l,h)=>{l.has(null)&&h._searchMarker&&(h._searchMarker.length=0)}),r=c},e);let o=e.currStackItem;if(o!=null){let c=r.changedParentTypes;e.emit("stack-item-popped",[{stackItem:o,type:n,changedParentTypes:c,origin:e},e]),e.currStackItem=null}return o},Ws=class extends Dt{constructor(t,{captureTimeout:n=500,captureTransaction:r=l=>!0,deleteFilter:s=()=>!0,trackedOrigins:i=new Set([null]),ignoreRemoteMapChanges:o=!1,doc:c=st(t)?t[0].doc:t instanceof Ct?t:t.doc}={}){super(),this.scope=[],this.doc=c,this.addToScope(t),this.deleteFilter=s,i.add(this),this.trackedOrigins=i,this.captureTransaction=r,this.undoStack=[],this.redoStack=[],this.undoing=!1,this.redoing=!1,this.currStackItem=null,this.lastChange=0,this.ignoreRemoteMapChanges=o,this.captureTimeout=n,this.afterTransactionHandler=l=>{if(!this.captureTransaction(l)||!this.scope.some(k=>l.changedParentTypes.has(k)||k===this.doc)||!this.trackedOrigins.has(l.origin)&&(!l.origin||!this.trackedOrigins.has(l.origin.constructor)))return;let h=this.undoing,a=this.redoing,u=h?this.redoStack:this.undoStack;h?this.stopCapturing():a||this.clear(!1,!0);let d=new gt;l.afterState.forEach((k,S)=>{let zr=l.beforeState.get(S)||0,Hr=k-zr;Hr>0&&Oe(d,S,zr,Hr)});let f=We(),p=!1;if(this.lastChange>0&&f-this.lastChange0&&!h&&!a){let k=u[u.length-1];k.deletions=Zt([k.deletions,l.deleteSet]),k.insertions=Zt([k.insertions,d])}else u.push(new Ur(l.deleteSet,d)),p=!0;!h&&!a&&(this.lastChange=f),Wt(l,l.deleteSet,k=>{k instanceof b&&this.scope.some(S=>S===l.doc||Sn(S,k))&&Gr(k,!0)});let y=[{stackItem:u[u.length-1],origin:l.origin,type:h?"redo":"undo",changedParentTypes:l.changedParentTypes},this];p?this.emit("stack-item-added",y):this.emit("stack-item-updated",y)},this.doc.on("afterTransaction",this.afterTransactionHandler),this.doc.on("destroy",()=>{this.destroy()})}addToScope(t){let n=new Set(this.scope);t=st(t)?t:[t],t.forEach(r=>{n.has(r)||(n.add(r),(r instanceof U?r.doc!==this.doc:r!==this.doc)&&yr("[yjs#509] Not same Y.Doc"),this.scope.push(r))})}addTrackedOrigin(t){this.trackedOrigins.add(t)}removeTrackedOrigin(t){this.trackedOrigins.delete(t)}clear(t=!0,n=!0){(t&&this.canUndo()||n&&this.canRedo())&&this.doc.transact(r=>{t&&(this.undoStack.forEach(s=>Ks(r,this,s)),this.undoStack=[]),n&&(this.redoStack.forEach(s=>Ks(r,this,s)),this.redoStack=[]),this.emit("stack-cleared",[{undoStackCleared:t,redoStackCleared:n}])})}stopCapturing(){this.lastChange=0}undo(){this.undoing=!0;let t;try{t=Ys(this,this.undoStack,"undo")}finally{this.undoing=!1}return t}redo(){this.redoing=!0;let t;try{t=Ys(this,this.redoStack,"redo")}finally{this.redoing=!1}return t}canUndo(){return this.undoStack.length>0}canRedo(){return this.redoStack.length>0}destroy(){this.trackedOrigins.delete(this),this.doc.off("afterTransaction",this.afterTransactionHandler),super.destroy()}};function*Dc(e){let t=w(e.restDecoder);for(let n=0;nTc(e,H),Tc=(e,t=j)=>{let n=[],r=new t(v(e)),s=new et(r,!1);for(let o=s.curr;o!==null;o=s.next())n.push(o);pn("Structs: ",n);let i=ot(r);pn("DeleteSet: ",i)},Va=e=>vc(e,H),vc=(e,t=j)=>{let n=[],r=new t(v(e)),s=new et(r,!1);for(let i=s.curr;i!==null;i=s.next())n.push(i);return{structs:n,ds:ot(r)}},Ne=class{constructor(t){this.currClient=0,this.startClock=0,this.written=0,this.encoder=t,this.clientStructs=[]}},Oc=e=>Un(e,H,ct),Vc=(e,t=Qt,n=j)=>{let r=new t,s=new et(new n(v(e)),!1),i=s.curr;if(i!==null){let o=0,c=i.id.client,l=i.id.clock!==0,h=l?0:i.id.clock+i.length;for(;i!==null;i=s.next())c!==i.id.client&&(h!==0&&(o++,g(r.restEncoder,c),g(r.restEncoder,h)),c=i.id.client,h=0,l=i.id.clock!==0),i.constructor===N&&(l=!0),l||(h=i.id.clock+i.length);h!==0&&(o++,g(r.restEncoder,c),g(r.restEncoder,h));let a=kt();return g(a,o),ns(a,r.restEncoder),r.restEncoder=a,r.toUint8Array()}else return g(r.restEncoder,0),r.toUint8Array()},Na=e=>Vc(e,mt,H),Nc=(e,t=j)=>{let n=new Map,r=new Map,s=new et(new t(v(e)),!1),i=s.curr;if(i!==null){let o=i.id.client,c=i.id.clock;for(n.set(o,c);i!==null;i=s.next())o!==i.id.client&&(r.set(o,c),n.set(i.id.client,i.id.clock),o=i.id.client),c=i.id.clock+i.length;r.set(o,c)}return{from:n,to:r}},La=e=>Nc(e,H),Lc=(e,t)=>{if(e.constructor===R){let{client:n,clock:r}=e.id;return new R(m(n,r+t),e.length-t)}else if(e.constructor===N){let{client:n,clock:r}=e.id;return new N(m(n,r+t),e.length-t)}else{let n=e,{client:r,clock:s}=n.id;return new b(m(r,s+t),null,m(r,s+t-1),null,n.rightOrigin,n.parent,n.parentSub,n.content.splice(t))}},Un=(e,t=j,n=X)=>{if(e.length===1)return e[0];let r=e.map(a=>new t(v(a))),s=r.map(a=>new et(a,!0)),i=null,o=new n,c=new Ne(o);for(;s=s.filter(d=>d.curr!==null),s.sort((d,f)=>{if(d.curr.id.client===f.curr.id.client){let p=d.curr.id.clock-f.curr.id.clock;return p===0?d.curr.constructor===f.curr.constructor?0:d.curr.constructor===N?1:-1:p}else return f.curr.id.client-d.curr.id.client}),s.length!==0;){let a=s[0],u=a.curr.id.client;if(i!==null){let d=a.curr,f=!1;for(;d!==null&&d.id.clock+d.length<=i.struct.id.clock+i.struct.length&&d.id.client>=i.struct.id.client;)d=a.next(),f=!0;if(d===null||d.id.client!==u||f&&d.id.clock>i.struct.id.clock+i.struct.length)continue;if(u!==i.struct.id.client)dt(c,i.struct,i.offset),i={struct:d,offset:0},a.next();else if(i.struct.id.clock+i.struct.length0&&(i.struct.constructor===N?i.struct.length-=p:d=Lc(d,p)),i.struct.mergeWith(d)||(dt(c,i.struct,i.offset),i={struct:d,offset:0},a.next())}}else i={struct:a.curr,offset:0},a.next();for(let d=a.curr;d!==null&&d.id.client===u&&d.id.clock===i.struct.id.clock+i.struct.length&&d.constructor!==N;d=a.next())dt(c,i.struct,i.offset),i={struct:d,offset:0}}i!==null&&(dt(c,i.struct,i.offset),i=null),$r(c);let l=r.map(a=>ot(a)),h=Zt(l);return tt(o,h),o.toUint8Array()},mi=(e,t,n=j,r=X)=>{let s=ci(t),i=new r,o=new Ne(i),c=new n(v(e)),l=new et(c,!1);for(;l.curr;){let a=l.curr,u=a.id.client,d=s.get(u)||0;if(l.curr.constructor===N){l.next();continue}if(a.id.clock+a.length>d)for(dt(o,a,it(d-a.id.clock,0)),l.next();l.curr&&l.curr.id.client===u;)dt(o,l.curr,0),l.next();else for(;l.curr&&l.curr.id.client===u&&l.curr.id.clock+l.curr.length<=d;)l.next()}$r(o);let h=ot(c);return tt(i,h),i.toUint8Array()},Ba=(e,t)=>mi(e,t,H,ct),wi=e=>{e.written>0&&(e.clientStructs.push({written:e.written,restEncoder:q(e.encoder.restEncoder)}),e.encoder.restEncoder=kt(),e.written=0)},dt=(e,t,n)=>{e.written>0&&e.currClient!==t.id.client&&wi(e),e.written===0&&(e.currClient=t.id.client,e.encoder.writeClient(t.id.client),g(e.encoder.restEncoder,t.id.clock+n)),t.write(e.encoder,n),e.written++},$r=e=>{wi(e);let t=e.encoder.restEncoder;g(t,e.clientStructs.length);for(let n=0;n{let s=new n(v(e)),i=new et(s,!1),o=new r,c=new Ne(o);for(let h=i.curr;h!==null;h=i.next())dt(c,t(h),0);$r(c);let l=ot(s);return tt(o,l),o.toUint8Array()},yi=({formatting:e=!0,subdocs:t=!0,yxml:n=!0}={})=>{let r=0,s=A(),i=A(),o=A(),c=A();return c.set(null,null),l=>{switch(l.constructor){case R:case N:return l;case b:{let h=l,a=h.content;switch(a.constructor){case Fe:break;case K:{if(n){let u=a.type;u instanceof se&&(u.nodeName=L(i,u.nodeName,()=>"node-"+r)),u instanceof In&&(u.hookName=L(i,u.hookName,()=>"hook-"+r))}break}case It:{let u=a;u.arr=u.arr.map(()=>r);break}case ie:{let u=a;u.content=new Uint8Array([r]);break}case oe:{let u=a;t&&(u.opts={},u.doc.guid=r+"");break}case wt:{let u=a;u.embed={};break}case C:{let u=a;e&&(u.key=L(o,u.key,()=>r+""),u.value=L(c,u.value,()=>({i:r})));break}case Dn:{let u=a;u.arr=u.arr.map(()=>r);break}case nt:{let u=a;u.str=Xe(r%10+"",u.str.length);break}default:T()}return h.parentSub&&(h.parentSub=L(s,h.parentSub,()=>r+"")),r++,l}default:T()}}},Ma=(e,t)=>vn(e,yi(t),H,ct),Ra=(e,t)=>vn(e,yi(t),j,X),$a=e=>vn(e,Zn,H,X),Bc=e=>vn(e,Zn,j,ct),Zs="You must not compute changes after the event-handler fired.",te=class{constructor(t,n){this.target=t,this.currentTarget=t,this.transaction=n,this._changes=null,this._keys=null,this._delta=null,this._path=null}get path(){return this._path||(this._path=Mc(this.currentTarget,this.target))}deletes(t){return ce(this.transaction.deleteSet,t.id)}get keys(){if(this._keys===null){if(this.transaction.doc._transactionCleanups.length===0)throw G(Zs);let t=new Map,n=this.target;this.transaction.changed.get(n).forEach(s=>{if(s!==null){let i=n._map.get(s),o,c;if(this.adds(i)){let l=i.left;for(;l!==null&&this.adds(l);)l=l.left;if(this.deletes(i))if(l!==null&&this.deletes(l))o="delete",c=Ge(l.content.getContent());else return;else l!==null&&this.deletes(l)?(o="update",c=Ge(l.content.getContent())):(o="add",c=void 0)}else if(this.deletes(i))o="delete",c=Ge(i.content.getContent());else return;t.set(s,{action:o,oldValue:c})}}),this._keys=t}return this._keys}get delta(){return this.changes.delta}adds(t){return t.id.clock>=(this.transaction.beforeState.get(t.id.client)||0)}get changes(){let t=this._changes;if(t===null){if(this.transaction.doc._transactionCleanups.length===0)throw G(Zs);let n=this.target,r=rt(),s=rt(),i=[];if(t={added:r,deleted:s,delta:i,keys:this.keys},this.transaction.changed.get(n).has(null)){let c=null,l=()=>{c&&i.push(c)};for(let h=n._start;h!==null;h=h.right)h.deleted?this.deletes(h)&&!this.adds(h)&&((c===null||c.delete===void 0)&&(l(),c={delete:0}),c.delete+=h.length,s.add(h)):this.adds(h)?((c===null||c.insert===void 0)&&(l(),c={insert:[]}),c.insert=c.insert.concat(h.content.getContent()),r.add(h)):((c===null||c.retain===void 0)&&(l(),c={retain:0}),c.retain+=h.length);c!==null&&c.retain===void 0&&l()}this._changes=t}return t}},Mc=(e,t)=>{let n=[];for(;t._item!==null&&t!==e;){if(t._item.parentSub!==null)n.unshift(t._item.parentSub);else{let r=0,s=t._item.parent._start;for(;s!==t._item&&s!==null;)!s.deleted&&s.countable&&(r+=s.length),s=s.right;n.unshift(r)}t=t._item.parent}return n},V=()=>{yr("Invalid access: Add Yjs type to a document before reading data.")},xi=80,Fr=0,Cr=class{constructor(t,n){t.marker=!0,this.p=t,this.index=n,this.timestamp=Fr++}},Rc=e=>{e.timestamp=Fr++},bi=(e,t,n)=>{e.p.marker=!1,e.p=t,t.marker=!0,e.index=n,e.timestamp=Fr++},$c=(e,t,n)=>{if(e.length>=xi){let r=e.reduce((s,i)=>s.timestamp{if(e._start===null||t===0||e._searchMarker===null)return null;let n=e._searchMarker.length===0?null:e._searchMarker.reduce((i,o)=>yt(t-i.index)t;)r=r.left,!r.deleted&&r.countable&&(s-=r.length);for(;r.left!==null&&r.left.id.client===r.id.client&&r.left.id.clock+r.left.length===r.id.clock;)r=r.left,!r.deleted&&r.countable&&(s-=r.length);return n!==null&&yt(n.index-s){for(let r=e.length-1;r>=0;r--){let s=e[r];if(n>0){let i=s.p;for(i.marker=!1;i&&(i.deleted||!i.countable);)i=i.left,i&&!i.deleted&&i.countable&&(s.index-=i.length);if(i===null||i.marker===!0){e.splice(r,1);continue}s.p=i,i.marker=!0}(t0&&t===s.index)&&(s.index=it(t,s.index+n))}},Fa=e=>{e.doc??V();let t=e._start,n=[];for(;t;)n.push(t),t=t.right;return n},Vn=(e,t,n)=>{let r=e,s=t.changedParentTypes;for(;L(s,e,()=>[]).push(n),e._item!==null;)e=e._item.parent;li(r._eH,n,t)},U=class{constructor(){this._item=null,this._map=new Map,this._start=null,this.doc=null,this._length=0,this._eH=Fs(),this._dEH=Fs(),this._searchMarker=null}get parent(){return this._item?this._item.parent:null}_integrate(t,n){this.doc=t,this._item=n}_copy(){throw z()}clone(){throw z()}_write(t){}get _first(){let t=this._start;for(;t!==null&&t.deleted;)t=t.right;return t}_callObserver(t,n){!t.local&&this._searchMarker&&(this._searchMarker.length=0)}observe(t){js(this._eH,t)}observeDeep(t){js(this._dEH,t)}unobserve(t){qs(this._eH,t)}unobserveDeep(t){qs(this._dEH,t)}toJSON(){}},ki=(e,t,n)=>{e.doc??V(),t<0&&(t=e._length+t),n<0&&(n=e._length+n);let r=n-t,s=[],i=e._start;for(;i!==null&&r>0;){if(i.countable&&!i.deleted){let o=i.content.getContent();if(o.length<=t)t-=o.length;else{for(let c=t;c0;c++)s.push(o[c]),r--;t=0}}i=i.right}return s},Si=e=>{e.doc??V();let t=[],n=e._start;for(;n!==null;){if(n.countable&&!n.deleted){let r=n.content.getContent();for(let s=0;s{let n=[],r=e._start;for(;r!==null;){if(r.countable&&ut(r,t)){let s=r.content.getContent();for(let i=0;i{let n=0,r=e._start;for(e.doc??V();r!==null;){if(r.countable&&!r.deleted){let s=r.content.getContent();for(let i=0;i{let n=[];return Be(e,(r,s)=>{n.push(t(r,s,e))}),n},Fc=e=>{let t=e._start,n=null,r=0;return{[Symbol.iterator](){return this},next:()=>{if(n===null){for(;t!==null&&t.deleted;)t=t.right;if(t===null)return{done:!0,value:void 0};n=t.content.getContent(),r=0,t=t.right}let s=n[r++];return n.length<=r&&(n=null),{done:!1,value:s}}}},Ei=(e,t)=>{e.doc??V();let n=On(e,t),r=e._start;for(n!==null&&(r=n.p,t-=n.index);r!==null;r=r.right)if(!r.deleted&&r.countable){if(t{let s=n,i=e.doc,o=i.clientID,c=i.store,l=n===null?t._start:n.right,h=[],a=()=>{h.length>0&&(s=new b(m(o,_(c,o)),s,s&&s.lastId,l,l&&l.id,t,null,new It(h)),s.integrate(e,0),h=[])};r.forEach(u=>{if(u===null)h.push(u);else switch(u.constructor){case Number:case Object:case Boolean:case Array:case String:h.push(u);break;default:switch(a(),u.constructor){case Uint8Array:case ArrayBuffer:s=new b(m(o,_(c,o)),s,s&&s.lastId,l,l&&l.id,t,null,new ie(new Uint8Array(u))),s.integrate(e,0);break;case Ct:s=new b(m(o,_(c,o)),s,s&&s.lastId,l,l&&l.id,t,null,new oe(u)),s.integrate(e,0);break;default:if(u instanceof U)s=new b(m(o,_(c,o)),s,s&&s.lastId,l,l&&l.id,t,null,new K(u)),s.integrate(e,0);else throw new Error("Unexpected content type in insert operation")}}}),a()},Ui=()=>G("Length exceeded!"),Ci=(e,t,n,r)=>{if(n>t._length)throw Ui();if(n===0)return t._searchMarker&&Le(t._searchMarker,n,r.length),Cn(e,t,null,r);let s=n,i=On(t,n),o=t._start;for(i!==null&&(o=i.p,n-=i.index,n===0&&(o=o.prev,n+=o&&o.countable&&!o.deleted?o.length:0));o!==null;o=o.right)if(!o.deleted&&o.countable){if(n<=o.length){n{let s=(t._searchMarker||[]).reduce((i,o)=>o.index>i.index?o:i,{index:0,p:t._start}).p;if(s)for(;s.right;)s=s.right;return Cn(e,t,s,n)},Ai=(e,t,n,r)=>{if(r===0)return;let s=n,i=r,o=On(t,n),c=t._start;for(o!==null&&(c=o.p,n-=o.index);c!==null&&n>0;c=c.right)!c.deleted&&c.countable&&(n0&&c!==null;)c.deleted||(r0)throw Ui();t._searchMarker&&Le(t._searchMarker,s,-i+r)},An=(e,t,n)=>{let r=t._map.get(n);r!==void 0&&r.delete(e)},jr=(e,t,n,r)=>{let s=t._map.get(n)||null,i=e.doc,o=i.clientID,c;if(r==null)c=new It([r]);else switch(r.constructor){case Number:case Object:case Boolean:case Array:case String:case Date:case BigInt:c=new It([r]);break;case Uint8Array:c=new ie(r);break;case Ct:c=new oe(r);break;default:if(r instanceof U)c=new K(r);else throw new Error("Unexpected content type")}new b(m(o,_(i.store,o)),s,s&&s.lastId,null,null,t,n,c).integrate(e,0)},qr=(e,t)=>{e.doc??V();let n=e._map.get(t);return n!==void 0&&!n.deleted?n.content.getContent()[n.length-1]:void 0},Ii=e=>{let t={};return e.doc??V(),e._map.forEach((n,r)=>{n.deleted||(t[r]=n.content.getContent()[n.length-1])}),t},Di=(e,t)=>{e.doc??V();let n=e._map.get(t);return n!==void 0&&!n.deleted},qa=(e,t,n)=>{let r=e._map.get(t)||null;for(;r!==null&&(!n.sv.has(r.id.client)||r.id.clock>=(n.sv.get(r.id.client)||0));)r=r.left;return r!==null&&ut(r,n)?r.content.getContent()[r.length-1]:void 0},qc=(e,t)=>{let n={};return e._map.forEach((r,s)=>{let i=r;for(;i!==null&&(!t.sv.has(i.id.client)||i.id.clock>=(t.sv.get(i.id.client)||0));)i=i.left;i!==null&&ut(i,t)&&(n[s]=i.content.getContent()[i.length-1])}),n},wn=e=>(e.doc??V(),Ls(e._map.entries(),t=>!t[1].deleted)),Ar=class extends te{},ee=class e extends U{constructor(){super(),this._prelimContent=[],this._searchMarker=[]}static from(t){let n=new e;return n.push(t),n}_integrate(t,n){super._integrate(t,n),this.insert(0,this._prelimContent),this._prelimContent=null}_copy(){return new e}clone(){let t=new e;return t.insert(0,this.toArray().map(n=>n instanceof U?n.clone():n)),t}get length(){return this.doc??V(),this._length}_callObserver(t,n){super._callObserver(t,n),Vn(this,t,new Ar(this,t))}insert(t,n){this.doc!==null?x(this.doc,r=>{Ci(r,this,t,n)}):this._prelimContent.splice(t,0,...n)}push(t){this.doc!==null?x(this.doc,n=>{jc(n,this,t)}):this._prelimContent.push(...t)}unshift(t){this.insert(0,t)}delete(t,n=1){this.doc!==null?x(this.doc,r=>{Ai(r,this,t,n)}):this._prelimContent.splice(t,n)}get(t){return Ei(this,t)}toArray(){return Si(this)}slice(t=0,n=this.length){return ki(this,t,n)}toJSON(){return this.map(t=>t instanceof U?t.toJSON():t)}map(t){return _i(this,t)}forEach(t){Be(this,t)}[Symbol.iterator](){return Fc(this)}_write(t){t.writeTypeRef(hl)}},Gc=e=>new ee,Ir=class extends te{constructor(t,n,r){super(t,n),this.keysChanged=r}},ne=class e extends U{constructor(t){super(),this._prelimContent=null,t===void 0?this._prelimContent=new Map:this._prelimContent=new Map(t)}_integrate(t,n){super._integrate(t,n),this._prelimContent.forEach((r,s)=>{this.set(s,r)}),this._prelimContent=null}_copy(){return new e}clone(){let t=new e;return this.forEach((n,r)=>{t.set(r,n instanceof U?n.clone():n)}),t}_callObserver(t,n){Vn(this,t,new Ir(this,t,n))}toJSON(){this.doc??V();let t={};return this._map.forEach((n,r)=>{if(!n.deleted){let s=n.content.getContent()[n.length-1];t[r]=s instanceof U?s.toJSON():s}}),t}get size(){return[...wn(this)].length}keys(){return gn(wn(this),t=>t[0])}values(){return gn(wn(this),t=>t[1].content.getContent()[t[1].length-1])}entries(){return gn(wn(this),t=>[t[0],t[1].content.getContent()[t[1].length-1]])}forEach(t){this.doc??V(),this._map.forEach((n,r)=>{n.deleted||t(n.content.getContent()[n.length-1],r,this)})}[Symbol.iterator](){return this.entries()}delete(t){this.doc!==null?x(this.doc,n=>{An(n,this,t)}):this._prelimContent.delete(t)}set(t,n){return this.doc!==null?x(this.doc,r=>{jr(r,this,t,n)}):this._prelimContent.set(t,n),n}get(t){return qr(this,t)}has(t){return Di(this,t)}clear(){this.doc!==null?x(this.doc,t=>{this.forEach(function(n,r,s){An(t,s,r)})}):this._prelimContent.clear()}_write(t){t.writeTypeRef(ul)}},zc=e=>new ne,ft=(e,t)=>e===t||typeof e=="object"&&typeof t=="object"&&e&&t&&Kn(e,t),Me=class{constructor(t,n,r,s){this.left=t,this.right=n,this.index=r,this.currentAttributes=s}forward(){switch(this.right===null&&T(),this.right.content.constructor){case C:this.right.deleted||le(this.currentAttributes,this.right.content);break;default:this.right.deleted||(this.index+=this.right.length);break}this.left=this.right,this.right=this.right.right}},Qs=(e,t,n)=>{for(;t.right!==null&&n>0;){switch(t.right.content.constructor){case C:t.right.deleted||le(t.currentAttributes,t.right.content);break;default:t.right.deleted||(n{let s=new Map,i=r?On(t,n):null;if(i){let o=new Me(i.p.left,i.p,i.index,s);return Qs(e,o,n-i.index)}else{let o=new Me(null,t._start,0,s);return Qs(e,o,n)}},Ti=(e,t,n,r)=>{for(;n.right!==null&&(n.right.deleted===!0||n.right.content.constructor===C&&ft(r.get(n.right.content.key),n.right.content.value));)n.right.deleted||r.delete(n.right.content.key),n.forward();let s=e.doc,i=s.clientID;r.forEach((o,c)=>{let l=n.left,h=n.right,a=new b(m(i,_(s.store,i)),l,l&&l.lastId,h,h&&h.id,t,null,new C(c,o));a.integrate(e,0),n.right=a,n.forward()})},le=(e,t)=>{let{key:n,value:r}=t;r===null?e.delete(n):e.set(n,r)},vi=(e,t)=>{for(;e.right!==null;){if(!(e.right.deleted||e.right.content.constructor===C&&ft(t[e.right.content.key]??null,e.right.content.value)))break;e.forward()}},Oi=(e,t,n,r)=>{let s=e.doc,i=s.clientID,o=new Map;for(let c in r){let l=r[c],h=n.currentAttributes.get(c)??null;if(!ft(h,l)){o.set(c,h);let{left:a,right:u}=n;n.right=new b(m(i,_(s.store,i)),a,a&&a.lastId,u,u&&u.id,t,null,new C(c,l)),n.right.integrate(e,0),n.forward()}}return o},xr=(e,t,n,r,s)=>{n.currentAttributes.forEach((d,f)=>{s[f]===void 0&&(s[f]=null)});let i=e.doc,o=i.clientID;vi(n,s);let c=Oi(e,t,n,s),l=r.constructor===String?new nt(r):r instanceof U?new K(r):new wt(r),{left:h,right:a,index:u}=n;t._searchMarker&&Le(t._searchMarker,n.index,l.getLength()),a=new b(m(o,_(i.store,o)),h,h&&h.lastId,a,a&&a.id,t,null,l),a.integrate(e,0),n.right=a,n.index=u,n.forward(),Ti(e,t,n,c)},ti=(e,t,n,r,s)=>{let i=e.doc,o=i.clientID;vi(n,s);let c=Oi(e,t,n,s);t:for(;n.right!==null&&(r>0||c.size>0&&(n.right.deleted||n.right.content.constructor===C));){if(!n.right.deleted)switch(n.right.content.constructor){case C:{let{key:l,value:h}=n.right.content,a=s[l];if(a!==void 0){if(ft(a,h))c.delete(l);else{if(r===0)break t;c.set(l,h)}n.right.delete(e)}else n.currentAttributes.set(l,h);break}default:r0){let l="";for(;r>0;r--)l+=`
+`;n.right=new b(m(o,_(i.store,o)),n.left,n.left&&n.left.lastId,n.right,n.right&&n.right.id,t,null,new nt(l)),n.right.integrate(e,0),n.forward()}Ti(e,t,n,c)},Vi=(e,t,n,r,s)=>{let i=t,o=A();for(;i&&(!i.countable||i.deleted);){if(!i.deleted&&i.content.constructor===C){let h=i.content;o.set(h.key,h)}i=i.right}let c=0,l=!1;for(;t!==i;){if(n===t&&(l=!0),!t.deleted){let h=t.content;switch(h.constructor){case C:{let{key:a,value:u}=h,d=r.get(a)??null;(o.get(a)!==h||d===u)&&(t.delete(e),c++,!l&&(s.get(a)??null)===u&&d!==u&&(d===null?s.delete(a):s.set(a,d))),!l&&!t.deleted&&le(s,h);break}}}t=t.right}return c},Hc=(e,t)=>{for(;t&&t.right&&(t.right.deleted||!t.right.countable);)t=t.right;let n=new Set;for(;t&&(t.deleted||!t.countable);){if(!t.deleted&&t.content.constructor===C){let r=t.content.key;n.has(r)?t.delete(e):n.add(r)}t=t.left}},Jc=e=>{let t=0;return x(e.doc,n=>{let r=e._start,s=e._start,i=A(),o=qe(i);for(;s;){if(s.deleted===!1)switch(s.content.constructor){case C:le(o,s.content);break;default:t+=Vi(n,r,s,i,o),i=qe(o),r=s;break}s=s.right}}),t},Xc=e=>{let t=new Set,n=e.doc;for(let[r,s]of e.afterState.entries()){let i=e.beforeState.get(r)||0;s!==i&&di(e,n.store.clients.get(r),i,s,o=>{!o.deleted&&o.content.constructor===C&&o.constructor!==R&&t.add(o.parent)})}x(n,r=>{Wt(e,e.deleteSet,s=>{if(s instanceof R||!s.parent._hasFormatting||t.has(s.parent))return;let i=s.parent;s.content.constructor===C?t.add(i):Hc(r,s)});for(let s of t)Jc(s)})},ei=(e,t,n)=>{let r=n,s=qe(t.currentAttributes),i=t.right;for(;n>0&&t.right!==null;){if(t.right.deleted===!1)switch(t.right.content.constructor){case K:case wt:case nt:n{s===null?this.childListChanged=!0:this.keysChanged.add(s)})}get changes(){if(this._changes===null){let t={keys:this.keys,delta:this.delta,added:new Set,deleted:new Set};this._changes=t}return this._changes}get delta(){if(this._delta===null){let t=this.target.doc,n=[];x(t,r=>{let s=new Map,i=new Map,o=this.target._start,c=null,l={},h="",a=0,u=0,d=()=>{if(c!==null){let f=null;switch(c){case"delete":u>0&&(f={delete:u}),u=0;break;case"insert":(typeof h=="object"||h.length>0)&&(f={insert:h},s.size>0&&(f.attributes={},s.forEach((p,y)=>{p!==null&&(f.attributes[y]=p)}))),h="";break;case"retain":a>0&&(f={retain:a},ds(l)||(f.attributes=hs({},l))),a=0;break}f&&n.push(f),c=null}};for(;o!==null;){switch(o.content.constructor){case K:case wt:this.adds(o)?this.deletes(o)||(d(),c="insert",h=o.content.getContent()[0],d()):this.deletes(o)?(c!=="delete"&&(d(),c="delete"),u+=1):o.deleted||(c!=="retain"&&(d(),c="retain"),a+=1);break;case nt:this.adds(o)?this.deletes(o)||(c!=="insert"&&(d(),c="insert"),h+=o.content.str):this.deletes(o)?(c!=="delete"&&(d(),c="delete"),u+=o.length):o.deleted||(c!=="retain"&&(d(),c="retain"),a+=o.length);break;case C:{let{key:f,value:p}=o.content;if(this.adds(o)){if(!this.deletes(o)){let y=s.get(f)??null;ft(y,p)?p!==null&&o.delete(r):(c==="retain"&&d(),ft(p,i.get(f)??null)?delete l[f]:l[f]=p)}}else if(this.deletes(o)){i.set(f,p);let y=s.get(f)??null;ft(y,p)||(c==="retain"&&d(),l[f]=y)}else if(!o.deleted){i.set(f,p);let y=l[f];y!==void 0&&(ft(y,p)?y!==null&&o.delete(r):(c==="retain"&&d(),p===null?delete l[f]:l[f]=p))}o.deleted||(c==="insert"&&d(),le(s,o.content));break}}o=o.right}for(d();n.length>0;){let f=n[n.length-1];if(f.retain!==void 0&&f.attributes===void 0)n.pop();else break}}),this._delta=n}return this._delta}},Re=class e extends U{constructor(t){super(),this._pending=t!==void 0?[()=>this.insert(0,t)]:[],this._searchMarker=[],this._hasFormatting=!1}get length(){return this.doc??V(),this._length}_integrate(t,n){super._integrate(t,n);try{this._pending.forEach(r=>r())}catch(r){console.error(r)}this._pending=null}_copy(){return new e}clone(){let t=new e;return t.applyDelta(this.toDelta()),t}_callObserver(t,n){super._callObserver(t,n);let r=new Dr(this,t,n);Vn(this,t,r),!t.local&&this._hasFormatting&&(t._needFormattingCleanup=!0)}toString(){this.doc??V();let t="",n=this._start;for(;n!==null;)!n.deleted&&n.countable&&n.content.constructor===nt&&(t+=n.content.str),n=n.right;return t}toJSON(){return this.toString()}applyDelta(t,{sanitize:n=!0}={}){this.doc!==null?x(this.doc,r=>{let s=new Me(null,this._start,0,new Map);for(let i=0;i0)&&xr(r,this,s,c,o.attributes||{})}else o.retain!==void 0?ti(r,this,s,o.retain,o.attributes||{}):o.delete!==void 0&&ei(r,s,o.delete)}}):this._pending.push(()=>this.applyDelta(t))}toDelta(t,n,r){this.doc??V();let s=[],i=new Map,o=this.doc,c="",l=this._start;function h(){if(c.length>0){let u={},d=!1;i.forEach((p,y)=>{d=!0,u[y]=p});let f={insert:c};d&&(f.attributes=u),s.push(f),c=""}}let a=()=>{for(;l!==null;){if(ut(l,t)||n!==void 0&&ut(l,n))switch(l.content.constructor){case nt:{let u=i.get("ychange");t!==void 0&&!ut(l,t)?(u===void 0||u.user!==l.id.client||u.type!=="removed")&&(h(),i.set("ychange",r?r("removed",l.id):{type:"removed"})):n!==void 0&&!ut(l,n)?(u===void 0||u.user!==l.id.client||u.type!=="added")&&(h(),i.set("ychange",r?r("added",l.id):{type:"added"})):u!==void 0&&(h(),i.delete("ychange")),c+=l.content.str;break}case K:case wt:{h();let u={insert:l.content.getContent()[0]};if(i.size>0){let d={};u.attributes=d,i.forEach((f,p)=>{d[p]=f})}s.push(u);break}case C:ut(l,t)&&(h(),le(i,l.content));break}l=l.right}h()};return t||n?x(o,u=>{t&&Sr(u,t),n&&Sr(u,n),a()},"cleanup"):a(),s}insert(t,n,r){if(n.length<=0)return;let s=this.doc;s!==null?x(s,i=>{let o=yn(i,this,t,!r);r||(r={},o.currentAttributes.forEach((c,l)=>{r[l]=c})),xr(i,this,o,n,r)}):this._pending.push(()=>this.insert(t,n,r))}insertEmbed(t,n,r){let s=this.doc;s!==null?x(s,i=>{let o=yn(i,this,t,!r);xr(i,this,o,n,r||{})}):this._pending.push(()=>this.insertEmbed(t,n,r||{}))}delete(t,n){if(n===0)return;let r=this.doc;r!==null?x(r,s=>{ei(s,yn(s,this,t,!0),n)}):this._pending.push(()=>this.delete(t,n))}format(t,n,r){if(n===0)return;let s=this.doc;s!==null?x(s,i=>{let o=yn(i,this,t,!1);o.right!==null&&ti(i,this,o,n,r)}):this._pending.push(()=>this.format(t,n,r))}removeAttribute(t){this.doc!==null?x(this.doc,n=>{An(n,this,t)}):this._pending.push(()=>this.removeAttribute(t))}setAttribute(t,n){this.doc!==null?x(this.doc,r=>{jr(r,this,t,n)}):this._pending.push(()=>this.setAttribute(t,n))}getAttribute(t){return qr(this,t)}getAttributes(){return Ii(this)}_write(t){t.writeTypeRef(dl)}},Pc=e=>new Re,ve=class{constructor(t,n=()=>!0){this._filter=n,this._root=t,this._currentNode=t._start,this._firstCall=!0,t.doc??V()}[Symbol.iterator](){return this}next(){let t=this._currentNode,n=t&&t.content&&t.content.type;if(t!==null&&(!this._firstCall||t.deleted||!this._filter(n)))do if(n=t.content.type,!t.deleted&&(n.constructor===se||n.constructor===re)&&n._start!==null)t=n._start;else for(;t!==null;){let r=t.next;if(r!==null){t=r;break}else t.parent===this._root?t=null:t=t.parent._item}while(t!==null&&(t.deleted||!this._filter(t.content.type)));return this._firstCall=!1,t===null?{value:void 0,done:!0}:(this._currentNode=t,{value:t.content.type,done:!1})}},re=class e extends U{constructor(){super(),this._prelimContent=[]}get firstChild(){let t=this._first;return t?t.content.getContent()[0]:null}_integrate(t,n){super._integrate(t,n),this.insert(0,this._prelimContent),this._prelimContent=null}_copy(){return new e}clone(){let t=new e;return t.insert(0,this.toArray().map(n=>n instanceof U?n.clone():n)),t}get length(){return this.doc??V(),this._prelimContent===null?this._length:this._prelimContent.length}createTreeWalker(t){return new ve(this,t)}querySelector(t){t=t.toUpperCase();let r=new ve(this,s=>s.nodeName&&s.nodeName.toUpperCase()===t).next();return r.done?null:r.value}querySelectorAll(t){return t=t.toUpperCase(),Y(new ve(this,n=>n.nodeName&&n.nodeName.toUpperCase()===t))}_callObserver(t,n){Vn(this,t,new Tr(this,n,t))}toString(){return _i(this,t=>t.toString()).join("")}toJSON(){return this.toString()}toDOM(t=document,n={},r){let s=t.createDocumentFragment();return r!==void 0&&r._createAssociation(s,this),Be(this,i=>{s.insertBefore(i.toDOM(t,n,r),null)}),s}insert(t,n){this.doc!==null?x(this.doc,r=>{Ci(r,this,t,n)}):this._prelimContent.splice(t,0,...n)}insertAfter(t,n){if(this.doc!==null)x(this.doc,r=>{let s=t&&t instanceof U?t._item:t;Cn(r,this,s,n)});else{let r=this._prelimContent,s=t===null?0:r.findIndex(i=>i===t)+1;if(s===0&&t!==null)throw G("Reference item not found");r.splice(s,0,...n)}}delete(t,n=1){this.doc!==null?x(this.doc,r=>{Ai(r,this,t,n)}):this._prelimContent.splice(t,n)}toArray(){return Si(this)}push(t){this.insert(this.length,t)}unshift(t){this.insert(0,t)}get(t){return Ei(this,t)}slice(t=0,n=this.length){return ki(this,t,n)}forEach(t){Be(this,t)}_write(t){t.writeTypeRef(pl)}},Kc=e=>new re,se=class e extends re{constructor(t="UNDEFINED"){super(),this.nodeName=t,this._prelimAttrs=new Map}get nextSibling(){let t=this._item?this._item.next:null;return t?t.content.type:null}get prevSibling(){let t=this._item?this._item.prev:null;return t?t.content.type:null}_integrate(t,n){super._integrate(t,n),this._prelimAttrs.forEach((r,s)=>{this.setAttribute(s,r)}),this._prelimAttrs=null}_copy(){return new e(this.nodeName)}clone(){let t=new e(this.nodeName),n=this.getAttributes();return us(n,(r,s)=>{t.setAttribute(s,r)}),t.insert(0,this.toArray().map(r=>r instanceof U?r.clone():r)),t}toString(){let t=this.getAttributes(),n=[],r=[];for(let c in t)r.push(c);r.sort();let s=r.length;for(let c=0;c0?" "+n.join(" "):"";return`<${i}${o}>${super.toString()}${i}>`}removeAttribute(t){this.doc!==null?x(this.doc,n=>{An(n,this,t)}):this._prelimAttrs.delete(t)}setAttribute(t,n){this.doc!==null?x(this.doc,r=>{jr(r,this,t,n)}):this._prelimAttrs.set(t,n)}getAttribute(t){return qr(this,t)}hasAttribute(t){return Di(this,t)}getAttributes(t){return t?qc(this,t):Ii(this)}toDOM(t=document,n={},r){let s=t.createElement(this.nodeName),i=this.getAttributes();for(let o in i){let c=i[o];typeof c=="string"&&s.setAttribute(o,c)}return Be(this,o=>{s.appendChild(o.toDOM(t,n,r))}),r!==void 0&&r._createAssociation(s,this),s}_write(t){t.writeTypeRef(fl),t.writeKey(this.nodeName)}},Yc=e=>new se(e.readKey()),Tr=class extends te{constructor(t,n,r){super(t,r),this.childListChanged=!1,this.attributesChanged=new Set,n.forEach(s=>{s===null?this.childListChanged=!0:this.attributesChanged.add(s)})}},In=class e extends ne{constructor(t){super(),this.hookName=t}_copy(){return new e(this.hookName)}clone(){let t=new e(this.hookName);return this.forEach((n,r)=>{t.set(r,n)}),t}toDOM(t=document,n={},r){let s=n[this.hookName],i;return s!==void 0?i=s.createDom(this):i=document.createElement(this.hookName),i.setAttribute("data-yjs-hook",this.hookName),r!==void 0&&r._createAssociation(i,this),i}_write(t){t.writeTypeRef(gl),t.writeKey(this.hookName)}},Wc=e=>new In(e.readKey()),vr=class e extends Re{get nextSibling(){let t=this._item?this._item.next:null;return t?t.content.type:null}get prevSibling(){let t=this._item?this._item.prev:null;return t?t.content.type:null}_copy(){return new e}clone(){let t=new e;return t.applyDelta(this.toDelta()),t}toDOM(t=document,n,r){let s=t.createTextNode(this.toString());return r!==void 0&&r._createAssociation(s,this),s}toString(){return this.toDelta().map(t=>{let n=[];for(let s in t.attributes){let i=[];for(let o in t.attributes[s])i.push({key:o,value:t.attributes[s][o]});i.sort((o,c)=>o.keys.nodeName"}r+=t.insert;for(let s=n.length-1;s>=0;s--)r+=`${n[s].nodeName}>`;return r}).join("")}toJSON(){return this.toString()}_write(t){t.writeTypeRef(ml)}},Zc=e=>new vr,$e=class{constructor(t,n){this.id=t,this.length=n}get deleted(){throw z()}mergeWith(t){return!1}write(t,n,r){throw z()}integrate(t,n){throw z()}},Qc=0,R=class extends $e{get deleted(){return!0}delete(){}mergeWith(t){return this.constructor!==t.constructor?!1:(this.length+=t.length,!0)}integrate(t,n){n>0&&(this.id.clock+=n,this.length-=n),ui(t.doc.store,this)}write(t,n){t.writeInfo(Qc),t.writeLen(this.length-n)}getMissing(t,n){return null}},ie=class e{constructor(t){this.content=t}getLength(){return 1}getContent(){return[this.content]}isCountable(){return!0}copy(){return new e(this.content)}splice(t){throw z()}mergeWith(t){return!1}integrate(t,n){}delete(t){}gc(t){}write(t,n){t.writeBuf(this.content)}getRef(){return 3}},tl=e=>new ie(e.readBuf()),Fe=class e{constructor(t){this.len=t}getLength(){return this.len}getContent(){return[]}isCountable(){return!1}copy(){return new e(this.len)}splice(t){let n=new e(this.len-t);return this.len=t,n}mergeWith(t){return this.len+=t.len,!0}integrate(t,n){Oe(t.deleteSet,n.id.client,n.id.clock,this.len),n.markDeleted()}delete(t){}gc(t){}write(t,n){t.writeLen(this.len-n)}getRef(){return 1}},el=e=>new Fe(e.readLen()),Ni=(e,t)=>new Ct({guid:e,...t,shouldLoad:t.shouldLoad||t.autoLoad||!1}),oe=class e{constructor(t){t._item&&console.error("This document was already integrated as a sub-document. You should create a second instance instead with the same guid."),this.doc=t;let n={};this.opts=n,t.gc||(n.gc=!1),t.autoLoad&&(n.autoLoad=!0),t.meta!==null&&(n.meta=t.meta)}getLength(){return 1}getContent(){return[this.doc]}isCountable(){return!0}copy(){return new e(Ni(this.doc.guid,this.opts))}splice(t){throw z()}mergeWith(t){return!1}integrate(t,n){this.doc._item=n,t.subdocsAdded.add(this.doc),this.doc.shouldLoad&&t.subdocsLoaded.add(this.doc)}delete(t){t.subdocsAdded.has(this.doc)?t.subdocsAdded.delete(this.doc):t.subdocsRemoved.add(this.doc)}gc(t){}write(t,n){t.writeString(this.doc.guid),t.writeAny(this.opts)}getRef(){return 9}},nl=e=>new oe(Ni(e.readString(),e.readAny())),wt=class e{constructor(t){this.embed=t}getLength(){return 1}getContent(){return[this.embed]}isCountable(){return!0}copy(){return new e(this.embed)}splice(t){throw z()}mergeWith(t){return!1}integrate(t,n){}delete(t){}gc(t){}write(t,n){t.writeJSON(this.embed)}getRef(){return 5}},rl=e=>new wt(e.readJSON()),C=class e{constructor(t,n){this.key=t,this.value=n}getLength(){return 1}getContent(){return[]}isCountable(){return!1}copy(){return new e(this.key,this.value)}splice(t){throw z()}mergeWith(t){return!1}integrate(t,n){let r=n.parent;r._searchMarker=null,r._hasFormatting=!0}delete(t){}gc(t){}write(t,n){t.writeKey(this.key),t.writeJSON(this.value)}getRef(){return 6}},sl=e=>new C(e.readKey(),e.readJSON()),Dn=class e{constructor(t){this.arr=t}getLength(){return this.arr.length}getContent(){return this.arr}isCountable(){return!0}copy(){return new e(this.arr)}splice(t){let n=new e(this.arr.slice(t));return this.arr=this.arr.slice(0,t),n}mergeWith(t){return this.arr=this.arr.concat(t.arr),!0}integrate(t,n){}delete(t){}gc(t){}write(t,n){let r=this.arr.length;t.writeLen(r-n);for(let s=n;s{let t=e.readLen(),n=[];for(let r=0;r{let t=e.readLen(),n=[];for(let r=0;r=55296&&r<=56319&&(this.str=this.str.slice(0,t-1)+"\uFFFD",n.str="\uFFFD"+n.str.slice(1)),n}mergeWith(t){return this.str+=t.str,!0}integrate(t,n){}delete(t){}gc(t){}write(t,n){t.writeString(n===0?this.str:this.str.slice(n))}getRef(){return 4}},ll=e=>new nt(e.readString()),al=[Gc,zc,Pc,Yc,Kc,Wc,Zc],hl=0,ul=1,dl=2,fl=3,pl=4,gl=5,ml=6,K=class e{constructor(t){this.type=t}getLength(){return 1}getContent(){return[this.type]}isCountable(){return!0}copy(){return new e(this.type._copy())}splice(t){throw z()}mergeWith(t){return!1}integrate(t,n){this.type._integrate(t.doc,n)}delete(t){let n=this.type._start;for(;n!==null;)n.deleted?n.id.clock<(t.beforeState.get(n.id.client)||0)&&t._mergeStructs.push(n):n.delete(t),n=n.right;this.type._map.forEach(r=>{r.deleted?r.id.clock<(t.beforeState.get(r.id.client)||0)&&t._mergeStructs.push(r):r.delete(t)}),t.changed.delete(this.type)}gc(t){let n=this.type._start;for(;n!==null;)n.gc(t,!0),n=n.right;this.type._start=null,this.type._map.forEach(r=>{for(;r!==null;)r.gc(t,!0),r=r.left}),this.type._map=new Map}write(t,n){this.type._write(t)}getRef(){return 7}},wl=e=>new K(al[e.readTypeRef()](e)),Or=(e,t)=>{let n=t,r=0,s;do r>0&&(n=m(n.client,n.clock+r)),s=Kt(e,n),r=n.clock-s.id.clock,n=s.redone;while(n!==null&&s instanceof b);return{item:s,diff:r}},Gr=(e,t)=>{for(;e!==null&&e.keep!==t;)e.keep=t,e=e.parent._item},Tn=(e,t,n)=>{let{client:r,clock:s}=t.id,i=new b(m(r,s+n),t,m(r,s+n-1),t.right,t.rightOrigin,t.parent,t.parentSub,t.content.splice(n));return t.deleted&&i.markDeleted(),t.keep&&(i.keep=!0),t.redone!==null&&(i.redone=m(t.redone.client,t.redone.clock+n)),t.right=i,i.right!==null&&(i.right.left=i),e._mergeStructs.push(i),i.parentSub!==null&&i.right===null&&i.parent._map.set(i.parentSub,i),t.length=n,i},ni=(e,t)=>he(e,n=>ce(n.deletions,t)),Li=(e,t,n,r,s,i)=>{let o=e.doc,c=o.store,l=o.clientID,h=t.redone;if(h!==null)return M(e,h);let a=t.parent._item,u=null,d;if(a!==null&&a.deleted===!0){if(a.redone===null&&(!n.has(a)||Li(e,a,n,r,s,i)===null))return null;for(;a.redone!==null;)a=M(e,a.redone)}let f=a===null?t.parent:a.content.type;if(t.parentSub===null){for(u=t.left,d=t;u!==null;){let S=u;for(;S!==null&&S.parent._item!==a;)S=S.redone===null?null:M(e,S.redone);if(S!==null&&S.parent._item===a){u=S;break}u=u.left}for(;d!==null;){let S=d;for(;S!==null&&S.parent._item!==a;)S=S.redone===null?null:M(e,S.redone);if(S!==null&&S.parent._item===a){d=S;break}d=d.right}}else{if(d=null,t.right&&!s){for(u=t;u!==null&&u.right!==null&&(u.right.redone||ce(r,u.right.id)||ni(i.undoStack,u.right.id)||ni(i.redoStack,u.right.id));)for(u=u.right;u.redone;)u=M(e,u.redone);if(u&&u.right!==null)return null}else u=f._map.get(t.parentSub)||null;u!==null&&u.parent._item!==a&&(u=f._map.get(t.parentSub)||null)}let p=_(c,l),y=m(l,p),k=new b(y,u,u&&u.lastId,d,d&&d.id,f,t.parentSub,t.content.copy());return t.redone=y,Gr(k,!0),k.integrate(e,0),k},b=class e extends $e{constructor(t,n,r,s,i,o,c,l){super(t,l.getLength()),this.origin=r,this.left=n,this.right=s,this.rightOrigin=i,this.parent=o,this.parentSub=c,this.redone=null,this.content=l,this.info=this.content.isCountable()?2:0}set marker(t){(this.info&8)>0!==t&&(this.info^=8)}get marker(){return(this.info&8)>0}get keep(){return(this.info&1)>0}set keep(t){this.keep!==t&&(this.info^=1)}get countable(){return(this.info&2)>0}get deleted(){return(this.info&4)>0}set deleted(t){this.deleted!==t&&(this.info^=4)}markDeleted(){this.info|=4}getMissing(t,n){if(this.origin&&this.origin.client!==this.id.client&&this.origin.clock>=_(n,this.origin.client))return this.origin.client;if(this.rightOrigin&&this.rightOrigin.client!==this.id.client&&this.rightOrigin.clock>=_(n,this.rightOrigin.client))return this.rightOrigin.client;if(this.parent&&this.parent.constructor===pt&&this.id.client!==this.parent.client&&this.parent.clock>=_(n,this.parent.client))return this.parent.client;if(this.origin&&(this.left=Js(t,n,this.origin),this.origin=this.left.lastId),this.rightOrigin&&(this.right=M(t,this.rightOrigin),this.rightOrigin=this.right.id),this.left&&this.left.constructor===R||this.right&&this.right.constructor===R)this.parent=null;else if(!this.parent)this.left&&this.left.constructor===e?(this.parent=this.left.parent,this.parentSub=this.left.parentSub):this.right&&this.right.constructor===e&&(this.parent=this.right.parent,this.parentSub=this.right.parentSub);else if(this.parent.constructor===pt){let r=Kt(n,this.parent);r.constructor===R?this.parent=null:this.parent=r.content.type}return null}integrate(t,n){if(n>0&&(this.id.clock+=n,this.left=Js(t,t.doc.store,m(this.id.client,this.id.clock-1)),this.origin=this.left.lastId,this.content=this.content.splice(n),this.length-=n),this.parent){if(!this.left&&(!this.right||this.right.left!==null)||this.left&&this.left.right!==this.right){let r=this.left,s;if(r!==null)s=r.right;else if(this.parentSub!==null)for(s=this.parent._map.get(this.parentSub)||null;s!==null&&s.left!==null;)s=s.left;else s=this.parent._start;let i=new Set,o=new Set;for(;s!==null&&s!==this.right;){if(o.add(s),i.add(s),Pt(this.origin,s.origin)){if(s.id.client{r.p===t&&(r.p=this,!this.deleted&&this.countable&&(r.index-=this.length))}),t.keep&&(this.keep=!0),this.right=t.right,this.right!==null&&(this.right.left=this),this.length+=t.length,!0}return!1}delete(t){if(!this.deleted){let n=this.parent;this.countable&&this.parentSub===null&&(n._length-=this.length),this.markDeleted(),Oe(t.deleteSet,this.id.client,this.id.clock,this.length),Ps(t,n,this.parentSub),this.content.delete(t)}}gc(t,n){if(!this.deleted)throw T();this.content.gc(t),n?Ic(t,this,new R(this.id,this.length)):this.content=new Fe(this.length)}write(t,n){let r=n>0?m(this.id.client,this.id.clock+n-1):this.origin,s=this.rightOrigin,i=this.parentSub,o=this.content.getRef()&31|(r===null?0:128)|(s===null?0:64)|(i===null?0:32);if(t.writeInfo(o),r!==null&&t.writeLeftID(r),s!==null&&t.writeRightID(s),r===null&&s===null){let c=this.parent;if(c._item!==void 0){let l=c._item;if(l===null){let h=ai(c);t.writeParentInfo(!0),t.writeString(h)}else t.writeParentInfo(!1),t.writeLeftID(l.id)}else c.constructor===String?(t.writeParentInfo(!0),t.writeString(c)):c.constructor===pt?(t.writeParentInfo(!1),t.writeLeftID(c)):T();i!==null&&t.writeString(i)}this.content.write(t,n)}},Bi=(e,t)=>yl[t&31](e),yl=[()=>{T()},el,il,tl,ll,rl,sl,wl,cl,nl,()=>{T()}],xl=10,N=class extends $e{get deleted(){return!0}delete(){}mergeWith(t){return this.constructor!==t.constructor?!1:(this.length+=t.length,!0)}integrate(t,n){T()}write(t,n){t.writeInfo(xl),g(t.restEncoder,this.length-n)}getMissing(t,n){return null}},Mi=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:{},Ri="__ $YJS$ __";Mi[Ri]===!0&&console.error("Yjs was already imported. This breaks constructor checks and will lead to issues! - https://github.com/yjs/yjs/issues/438");Mi[Ri]=!0;export{kr as AbsolutePosition,Rs as AbstractConnector,$e as AbstractStruct,U as AbstractType,ee as Array,It as ContentAny,ie as ContentBinary,Fe as ContentDeleted,oe as ContentDoc,wt as ContentEmbed,C as ContentFormat,Dn as ContentJSON,nt as ContentString,K as ContentType,Ct as Doc,R as GC,pt as ID,b as Item,ne as Map,Hs as PermanentUserData,Ve as RelativePosition,N as Skip,_n as Snapshot,Re as Text,Er as Transaction,Ws as UndoManager,H as UpdateDecoderV1,j as UpdateDecoderV2,ct as UpdateEncoderV1,X as UpdateEncoderV2,se as XmlElement,re as XmlFragment,In as XmlHook,vr as XmlText,Ar as YArrayEvent,te as YEvent,Ir as YMapEvent,Dr as YTextEvent,Tr as YXmlEvent,fa as applyUpdate,Mr as applyUpdateV2,Jc as cleanupYTextFormatting,Pt as compareIDs,_a as compareRelativePositions,$a as convertUpdateFormatV1ToV2,Bc as convertUpdateFormatV2ToV1,Sa as createAbsolutePositionFromRelativePosition,Lr as createDeleteSet,ri as createDeleteSetFromStructStore,Da as createDocFromSnapshot,m as createID,ya as createRelativePositionFromJSON,xa as createRelativePositionFromTypeIndex,hi as createSnapshot,ka as decodeRelativePosition,Ca as decodeSnapshot,Uc as decodeSnapshotV2,ci as decodeStateVector,Va as decodeUpdate,vc as decodeUpdateV2,Ba as diffUpdate,mi as diffUpdateV2,Aa as emptySnapshot,ba as encodeRelativePosition,Ua as encodeSnapshot,Ec as encodeSnapshotV2,pa as encodeStateAsUpdate,wc as encodeStateAsUpdateV2,ga as encodeStateVector,Na as encodeStateVectorFromUpdate,Vc as encodeStateVectorFromUpdateV2,uc as equalDeleteSets,Ea as equalSnapshots,P as findIndexSS,ai as findRootTypeKey,Kt as getItem,Js as getItemCleanEnd,M as getItemCleanStart,_ as getState,Fa as getTypeChildren,ce as isDeleted,Sn as isParentOf,Wt as iterateDeletedStructs,ma as logType,Oa as logUpdate,Tc as logUpdateV2,Zt as mergeDeleteSets,Oc as mergeUpdates,Un as mergeUpdatesV2,Ma as obfuscateUpdate,Ra as obfuscateUpdateV2,La as parseUpdateMeta,Nc as parseUpdateMetaV2,da as readUpdate,ii as readUpdateV2,wa as relativePositionToJSON,Ia as snapshot,Ta as snapshotContainsUpdate,x as transact,va as tryGc,ja as typeListToArraySnapshot,qc as typeMapGetAllSnapshot,qa as typeMapGetSnapshot};
diff --git a/desktop-app/prepare.js b/desktop-app/prepare.js
index a9233fd5..c465bcae 100644
--- a/desktop-app/prepare.js
+++ b/desktop-app/prepare.js
@@ -49,6 +49,9 @@ console.log("✓ Copied script.js → resources/js/script.js");
fs.copyFileSync(path.join(ROOT_DIR, "workspace-storage.js"), path.join(jsDest, "workspace-storage.js"));
fs.copyFileSync(path.join(ROOT_DIR, "native-markdown-source.js"), path.join(jsDest, "native-markdown-source.js"));
fs.copyFileSync(path.join(ROOT_DIR, "browser-drop-source.js"), path.join(jsDest, "browser-drop-source.js"));
+for (const name of ['sharing-crypto.js', 'sharing-media.js', 'sharing-live.js']) {
+ fs.copyFileSync(path.join(ROOT_DIR, name), path.join(jsDest, name));
+}
console.log("✓ Copied workspace-storage.js → resources/js/workspace-storage.js");
if (fs.existsSync(path.join(ROOT_DIR, "visual-settings-store.js"))) {
@@ -352,22 +355,14 @@ async function prepareOfflineDependencies() {
html = html.replace(/');
html = html.replace(/');
-
- // Inject Neutralino script tags
- html = html.replace(
- /\n \n ';
- if (match.includes('visual-settings-adapters.js')) {
- tags += '\n ';
- }
- if (match.includes('visual-settings-store.js')) {
- tags += '\n ';
- }
- tags += '\n ';
- return tags;
- }
- );
+ html = html.replace(/');
+
+ // Preserve script order without requiring adjacent tags: sharing modules load
+ // between settings and the application entry point.
+ html = html.replace(/\n \n ');
+ html = html.replace(/');
// Explicit desktop demo opt-in; the official runtime pin remains unchanged.
// Default desktop and web builds
diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html
index 4cd27953..e64fa26b 100644
--- a/desktop-app/resources/index.html
+++ b/desktop-app/resources/index.html
@@ -1168,6 +1168,8 @@
Markdown Viewer
Copy
+
Encrypted sharing. Keep the complete link private; it includes the decryption key.
+
@@ -2357,6 +2359,9 @@
No comments yet
+
+
+
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index 5927d52b..400aea36 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -247,7 +247,7 @@ document.addEventListener("DOMContentLoaded", async function () {
markmapLib: 'https://cdn.jsdelivr.net/npm/markmap-lib@0.18.12/dist/browser/index.iife.js',
markmapView: 'https://cdn.jsdelivr.net/npm/markmap-view@0.18.12/dist/browser/index.js',
jszip: 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js',
- yjs: 'https://esm.sh/yjs@13.6.10/es2022/yjs.mjs'
+ yjs: './assets/yjs.mjs'
};
// Resolve local paths for desktop (Neutralinojs) offline support
@@ -270,6 +270,7 @@ document.addEventListener("DOMContentLoaded", async function () {
CDN.markmapLib = '/libs/markmap-lib.iife.js';
CDN.markmapView = '/libs/markmap-view.js';
CDN.jszip = '/libs/jszip.min.js';
+ CDN.yjs = '/assets/yjs.mjs';
}
// Active WebGL / Three.js 3D STL renderers Map for memory cleanup
@@ -545,6 +546,16 @@ document.addEventListener("DOMContentLoaded", async function () {
return Boolean(activeTabId && isShareSnapshotTabId(activeTabId));
}
+ function isEncryptedSharingActive() {
+ const tab = tabs.find(item => item.id === activeTabId);
+ return Boolean((tab && tab.encryptedSharing) ||
+ (liveCollaboration && liveCollaboration.encryption && liveCollaboration.tabId === activeTabId));
+ }
+
+ function prepareSharedPreviewHtml(html) {
+ return MarkdownShareMedia.prepareHtml(html, { base: getShareApiBaseUrl(), privateContent: isEncryptedSharingActive() });
+ }
+
function isReleaseNotesTab(tab) {
return Boolean(tab && tab.kind === RELEASE_NOTES_TAB_KIND);
}
@@ -1059,7 +1070,6 @@ document.addEventListener("DOMContentLoaded", async function () {
const EMBEDDED_IMAGE_MAX_DIMENSION = 1600;
const MANAGED_GIF_MAX_BYTES = 5 * 1024 * 1024;
const MANAGED_VIDEO_MAX_BYTES = 10 * 1024 * 1024;
- const MANAGED_IMAGE_ID_PATTERN = /^[A-Za-z0-9_-]{20,32}$/;
const managedImageMigrationOfferedTabIds = new Set();
const managedImageMigrationInProgressTabIds = new Set();
const EMOJI_API_URL = 'https://api.github.com/emojis';
@@ -3533,6 +3543,7 @@ document.addEventListener("DOMContentLoaded", async function () {
}
async function fetchDiagramSvgRequest(url, options, engine, attempts = DIAGRAM_REQUEST_RETRIES) {
+ if (isEncryptedSharingActive()) throw new Error('Remote rendering is disabled for encrypted shared documents.');
let lastError = null;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
const controller = new AbortController();
@@ -14992,6 +15003,7 @@ document.addEventListener("DOMContentLoaded", async function () {
}
function disposeDocumentSplitPreviewResources() {
+ if (documentSplitPreview) MarkdownShareMedia.release(documentSplitPreview);
if (!documentSplitPreview) return;
documentSplitPreview.querySelectorAll('.geojson-map, .topojson-map').forEach(function(node) {
disposeMapNode(node);
@@ -15023,14 +15035,17 @@ document.addEventListener("DOMContentLoaded", async function () {
const referenceData = extractReferenceDefinitions(parsed.body);
const html = tableHtml + marked.parse(referenceData.cleanedMarkdown);
disposeDocumentSplitPreviewResources();
- documentSplitPreview.innerHTML = sanitizePreviewHtml(html);
+ const privateContent = Boolean(tab.encryptedSharing || (liveCollaboration && liveCollaboration.encryption && liveCollaboration.tabId === tab.id));
+ documentSplitPreview.innerHTML = MarkdownShareMedia.prepareHtml(sanitizePreviewHtml(html), { base: getShareApiBaseUrl(), privateContent });
+ MarkdownShareMedia.resolve(documentSplitPreview, { base: getShareApiBaseUrl() }).catch(function() {});
applyReferencePreviewLinks(documentSplitPreview, referenceData.definitions);
enhanceGitHubAlerts(documentSplitPreview);
enhancePreviewCodeBlocks(documentSplitPreview);
- processEmojis(documentSplitPreview);
+ if (!privateContent) processEmojis(documentSplitPreview);
namespaceSplitPreviewIds(documentSplitPreview, tab.id);
documentSplitPreviewTabId = tab.id;
documentSplitPreviewContent = content;
+ if (privateContent) return;
const context = {
isCurrent: function() {
return (
@@ -16471,6 +16486,9 @@ document.addEventListener("DOMContentLoaded", async function () {
}
function commitPreviewHtml(sanitizedHtml, referenceData, rawVal, context) {
+ sanitizedHtml = prepareSharedPreviewHtml(sanitizedHtml);
+ const hasEncryptedMedia = sanitizedHtml.includes('data-encrypted-media=') || Boolean(markdownPreview.querySelector('[data-encrypted-media]'));
+ MarkdownShareMedia.release(markdownPreview);
const renderedDocumentId = previewLastRenderedTabId;
const renderedScrollSnapshot = captureRenderedPreviewScroll(renderedDocumentId);
if (renderedScrollSnapshot) {
@@ -16483,13 +16501,14 @@ document.addEventListener("DOMContentLoaded", async function () {
mathJaxTypesetRunId += 1;
clearMathJaxPreviewState(markdownPreview);
const patchResult = patchPreviewDom(markdownPreview, sanitizedHtml, {
- reusePreviewBlocks: context.previewEngineMode === 'segmented' && !context.force,
+ reusePreviewBlocks: context.previewEngineMode === 'segmented' && !context.force && !hasEncryptedMedia && !isEncryptedSharingActive(),
});
invalidateReviewTargetSourceSnapshots(
patchResult && patchResult.fullReplace ? [markdownPreview] : ((patchResult && patchResult.updatedNodes) || [])
);
applyReferencePreviewLinks(markdownPreview, referenceData.definitions);
enhanceGitHubAlerts(markdownPreview);
+ MarkdownShareMedia.resolve(markdownPreview, { base: getShareApiBaseUrl() }).catch(() => {});
_lastRenderedContent = rawVal;
previewHasCommittedRender = true;
@@ -17706,6 +17725,7 @@ ${selector} .arrowheadPath {
}
async function renderThemeSensitivePreviewContentForPrint() {
+ if (isEncryptedSharingActive()) return;
const context = {
renderId: previewRenderGeneration,
previewDocumentId: getActivePreviewDocumentId(),
@@ -17980,6 +18000,14 @@ ${selector} .arrowheadPath {
function postProcessPreview(rawVal, context, patchResult) {
const roots = getPreviewPostProcessRoots(patchResult, context);
+ if (isEncryptedSharingActive()) {
+ roots.forEach(root => { enhancePreviewCodeBlocks(root); enhanceGitHubAlerts(root); });
+ refreshDocumentOutline();
+ decorateReviewTargets();
+ updateFindHighlights();
+ scheduleLineNumberUpdate();
+ return;
+ }
// Clean up orphaned STL views that are no longer present in the document
activeStlViews.forEach((view, id) => {
@@ -20793,7 +20821,7 @@ ${selector} .arrowheadPath {
async function requestManagedImageUploadConsent() {
if (loadGlobalState().managedImageUploadAcknowledged === true) return true;
const accepted = await showAppToastConfirmation(
- 'Images, GIFs, and videos are uploaded to Markdown Viewer public media storage for up to 90 days. Anyone with a media URL can view it during that time.',
+ 'Images, GIFs, and videos are encrypted on this device before upload and kept for up to 90 days. Keep the complete media link private; it includes the key needed to open the media.',
{ title: 'Upload media?', confirmLabel: 'Continue', cancelLabel: 'Cancel', dedupeKey: 'managed-media-consent' }
);
if (accepted) saveGlobalState({ managedImageUploadAcknowledged: true });
@@ -20802,12 +20830,15 @@ ${selector} .arrowheadPath {
async function uploadManagedImageDataUrl(dataUrl, endpoint) {
const apiPath = endpoint === '/api/media' ? '/api/media' : '/api/image';
+ MarkdownShareMedia.validateDataUrl(dataUrl);
+ const mediaKey = MarkdownShareCrypto.random();
+ const encrypted = await MarkdownShareCrypto.seal({ dataUrl }, mediaKey, 'media', MarkdownShareCrypto.MEDIA_LIMIT);
let response;
try {
response = await fetch(getShareApiBaseUrl() + apiPath, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ dataUrl: dataUrl })
+ body: JSON.stringify({ encrypted })
});
} catch (_) {
throw new Error('The media file could not be uploaded. Check your connection and try again, or use External Media (URL).');
@@ -20820,7 +20851,7 @@ ${selector} .arrowheadPath {
if (!response.ok) {
throw new Error((payload && payload.error) || 'The media host rejected this file. Try a smaller file or use External Media (URL).');
}
- if (!payload || !MANAGED_IMAGE_ID_PATTERN.test(payload.id || '')) {
+ if (!payload || !/^e2_[A-Za-z0-9_-]{24}$/.test(payload.id || '')) {
throw new Error('The media host returned an invalid short link. Please try again.');
}
@@ -20835,6 +20866,8 @@ ${selector} .arrowheadPath {
if (managedUrl.origin !== expectedBase.origin || managedUrl.pathname !== apiPath + '/' + payload.id) {
throw new Error('The media host returned an invalid short link. Please try again.');
}
+ managedUrl.search = '';
+ managedUrl.hash = 'key=' + mediaKey;
return managedUrl.toString();
}
@@ -28847,7 +28880,7 @@ ${selector} .arrowheadPath {
}
});
- exportMd.addEventListener("click", function () {
+ exportMd.addEventListener("click", async function () {
if (!hasActiveOpenDocument()) return;
if (isReleaseNotesActive()) return;
if (blockShareSnapshotSourceAccess()) return;
@@ -28856,7 +28889,8 @@ ${selector} .arrowheadPath {
return;
}
try {
- const blob = new Blob([markdownEditor.value], {
+ const portableMarkdown = await MarkdownShareMedia.inlineMarkdown(markdownEditor.value, getShareApiBaseUrl());
+ const blob = new Blob([portableMarkdown], {
type: "text/markdown;charset=utf-8",
});
saveAs(blob, getExportFilename("md", "document.md"));
@@ -28920,10 +28954,11 @@ ${selector} .arrowheadPath {
generateHtmlExport(selectedTheme);
});
- function generateHtmlExport(exportTheme = 'light') {
+ async function generateHtmlExport(exportTheme = 'light') {
if (!hasActiveOpenDocument()) return;
if (isReleaseNotesActive()) return;
try {
+ const privateExport = isEncryptedSharingActive();
const { frontmatter, body } = parseFrontmatter(markdownEditor.value);
const tableHtml = frontmatter ? renderFrontmatterTable(frontmatter) : '';
const referenceData = extractReferenceDefinitions(body);
@@ -28934,7 +28969,13 @@ ${selector} .arrowheadPath {
ALLOWED_URI_REGEXP: SAFE_MARKDOWN_URI_REGEXP
});
const tempContainer = document.createElement("div");
- tempContainer.innerHTML = sanitizedHtml;
+ tempContainer.innerHTML = prepareSharedPreviewHtml(sanitizedHtml);
+ await MarkdownShareMedia.resolve(tempContainer, { base: getShareApiBaseUrl(), inline: true });
+ if (privateExport) {
+ const secureHtml = 'Shared document' + tempContainer.innerHTML + '';
+ saveAs(new Blob([secureHtml], { type: 'text/html;charset=utf-8' }), getExportFilename('html', 'document.html'));
+ return;
+ }
applyReferencePreviewLinks(tempContainer, referenceData.definitions);
enhanceGitHubAlerts(tempContainer);
tempContainer.querySelectorAll('.diagram-status, .diagram-toolbar, .mermaid-toolbar, .abc-toolbar').forEach(function(el) {
@@ -29832,6 +29873,7 @@ ${selector} .arrowheadPath {
function disposeExportRenderElement(root) {
if (!root) return;
+ MarkdownShareMedia.release(root);
root.querySelectorAll('.geojson-map, .topojson-map').forEach(disposeMapNode);
root.querySelectorAll('.stl-viewer[id]').forEach(node => disposeStlView(node.id));
clearMathJaxPreviewState(root);
@@ -29843,6 +29885,11 @@ ${selector} .arrowheadPath {
}
async function renderExportRichContent(root, markdown, exportTheme, state) {
+ await MarkdownShareMedia.resolve(root, { base: getShareApiBaseUrl() });
+ if (isEncryptedSharingActive()) {
+ await waitForExportWork(state, waitForAllImages(root));
+ return;
+ }
const theme = getExportTheme(exportTheme);
const context = {
theme,
@@ -29888,7 +29935,7 @@ ${selector} .arrowheadPath {
root.className = `markdown-body pdf-export theme-${theme} export-render-root`;
root.setAttribute('data-theme', theme);
root.setAttribute('data-export-render-mode', mode);
- root.innerHTML = html;
+ root.innerHTML = prepareSharedPreviewHtml(html);
root.style.boxSizing = 'border-box';
root.style.position = 'fixed';
root.style.left = '-12000px';
@@ -30376,18 +30423,8 @@ ${selector} .arrowheadPath {
const MAX_SHARE_URL_LENGTH = 32000;
const MAX_LEGACY_SHARE_URL_LENGTH = 4096;
const SERVER_SHARE_THRESHOLD_BYTES = 3000;
- const SERVER_SHARE_ID_PATTERN = /^[a-z2-9]{6,20}$/;
+ const SERVER_SHARE_ID_PATTERN = /^(?:[a-z2-9]{6,20}|s2_[A-Za-z0-9_-]{24})$/;
- function encodeMarkdownForShare(text) {
- if (typeof pako === 'undefined') throw new Error('pako not loaded');
- const compressed = pako.deflate(new TextEncoder().encode(text));
- const chunkSize = 0x8000;
- let binary = '';
- for (let i = 0; i < compressed.length; i += chunkSize) {
- binary += String.fromCharCode.apply(null, compressed.subarray(i, i + chunkSize));
- }
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
- }
function decodeMarkdownFromShare(encoded) {
if (typeof pako === 'undefined') throw new Error('pako not loaded');
@@ -30431,13 +30468,14 @@ ${selector} .arrowheadPath {
return cleanTitle || 'Shared Snapshot';
}
- async function openShareSnapshotTab(content, title, mode) {
+ async function openShareSnapshotTab(content, title, mode, encrypted = false) {
const shareMode = mode === 'edit' ? 'edit' : 'view';
const viewMode = shareMode === 'edit' ? 'split' : 'preview';
const snapshotTab = createTab(typeof content === 'string' ? content : '', getSafeShareSnapshotTitle(title), viewMode);
snapshotTab.kind = SHARE_SNAPSHOT_TAB_KIND;
snapshotTab.temporary = true;
snapshotTab.shareSnapshotMode = shareMode;
+ snapshotTab.encryptedSharing = encrypted;
tabs.push(snapshotTab);
await switchTab(snapshotTab.id);
applyShareSnapshotAccessMode(shareMode);
@@ -30447,18 +30485,11 @@ ${selector} .arrowheadPath {
return true;
}
- function getShareHashForMode(id, mode) {
- return 'id=' + encodeURIComponent(id) + (mode === 'edit' ? '&edit=1' : '');
- }
function getShareSnapshotTitleParam(title) {
return '&title=' + encodeURIComponent(getSafeShareSnapshotTitle(title));
}
- function shouldUseServerShare(markdownText, legacyUrl) {
- const byteLength = new TextEncoder().encode(markdownText || '').length;
- return byteLength >= SERVER_SHARE_THRESHOLD_BYTES || !legacyUrl || legacyUrl.length > MAX_LEGACY_SHARE_URL_LENGTH;
- }
// ============================================
// Share Modal
@@ -30475,6 +30506,7 @@ ${selector} .arrowheadPath {
const shareUrlInput = document.getElementById('share-url-input');
const shareGenerateBtn = document.getElementById('share-generate-btn');
const shareCopyBtn = document.getElementById('share-copy-btn');
+ const shareDeleteBtn = document.getElementById('share-delete-btn');
const shareInviteState = document.getElementById('share-invite-state');
const shareModeView = document.getElementById('share-mode-view');
const shareModeEdit = document.getElementById('share-mode-edit');
@@ -30485,34 +30517,24 @@ ${selector} .arrowheadPath {
let generatedShareSnapshotId = '';
let generatedShareSnapshotDeleteToken = '';
- function buildShareUrl(mode) {
+ async function buildShareUrl(mode) {
generatedShareSnapshotExpiresIn = null;
generatedShareSnapshotId = '';
generatedShareSnapshotDeleteToken = '';
- const markdownText = markdownEditor.value;
- let encoded;
- try {
- encoded = encodeMarkdownForShare(markdownText);
- } catch (e) {
- console.error('Share encoding failed:', e);
- return null;
- }
- const base = getPublicAppBaseUrl() + '#share=' + encoded + getShareSnapshotTitleParam(getActiveShareTitle());
- return mode === 'edit' ? base + '&edit=1' : base;
+ const key = MarkdownShareCrypto.random();
+ const payload = { content: markdownEditor.value || '', title: getActiveShareTitle(), mode };
+ const encrypted = await MarkdownShareCrypto.seal(payload, key, 'snapshot');
+ const inlineUrl = getPublicAppBaseUrl() + '#share=v2.' + MarkdownShareCrypto.pack(encrypted) + '&key=' + key;
+ return { key, encrypted, inlineUrl, byteLength: new TextEncoder().encode(payload.content).length };
}
- async function createStoredShareUrl(mode) {
- const markdownText = markdownEditor.value || '';
+ async function createStoredShareUrl(prepared) {
const response = await fetch(getShareApiBaseUrl() + '/api/share', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
- body: JSON.stringify({
- content: markdownText,
- mode,
- title: getActiveShareTitle()
- })
+ body: JSON.stringify({ encrypted: prepared.encrypted })
});
let payload = null;
@@ -30523,14 +30545,14 @@ ${selector} .arrowheadPath {
if (!response.ok) {
throw new Error((payload && payload.error) || ('HTTP ' + response.status));
}
- if (!payload || !SERVER_SHARE_ID_PATTERN.test(payload.id || '')) {
+ if (!payload || !/^s2_[A-Za-z0-9_-]{24}$/.test(payload.id || '')) {
throw new Error('Invalid share id returned');
}
generatedShareSnapshotExpiresIn = Number.isFinite(payload.expiresIn) ? payload.expiresIn : null;
generatedShareSnapshotId = payload.id;
generatedShareSnapshotDeleteToken = typeof payload.deleteToken === 'string' ? payload.deleteToken : '';
- return getPublicAppBaseUrl() + '#' + getShareHashForMode(payload.id, mode);
+ return getPublicAppBaseUrl() + '#id=' + payload.id + '&key=' + prepared.key;
}
function formatShareExpiry(seconds) {
@@ -30562,6 +30584,7 @@ ${selector} .arrowheadPath {
}
function resetShareSnapshotLink() {
+ if (shareDeleteBtn) shareDeleteBtn.hidden = true;
generatedShareSnapshotUrl = '';
generatedShareSnapshotExpiresIn = null;
generatedShareSnapshotId = '';
@@ -30585,16 +30608,11 @@ ${selector} .arrowheadPath {
async function createShareSnapshotLink() {
const mode = getCurrentShareMode();
- const url = buildShareUrl(mode);
- if (!url) {
- shareUrlInput.value = 'Error generating link.';
- shareCopyBtn.disabled = true;
- throw new Error('Unable to create share link');
- }
- if (shouldUseServerShare(markdownEditor.value || '', url)) {
- return await createStoredShareUrl(mode);
+ const prepared = await buildShareUrl(mode);
+ if (prepared.byteLength >= SERVER_SHARE_THRESHOLD_BYTES || prepared.inlineUrl.length > MAX_LEGACY_SHARE_URL_LENGTH) {
+ return await createStoredShareUrl(prepared);
}
- return url;
+ return prepared.inlineUrl;
}
function openShareModal() {
@@ -30602,16 +30620,6 @@ ${selector} .arrowheadPath {
if (isReleaseNotesActive()) return;
if (blockTemporarySnapshotShare()) return;
if (blockLiveDocumentShareSnapshot()) return;
- // PERF-002: Lazy-load pako on first share
- if (typeof pako === 'undefined') {
- loadScript(CDN.pako).then(function() {
- openShareModal();
- }).catch(function(e) {
- console.error('Failed to load pako:', e);
- alert('Failed to load sharing library. Please check your internet connection.');
- });
- return;
- }
// Reset to view-only by default each time
shareModeView.checked = true;
syncShareCardStyles();
@@ -30657,6 +30665,7 @@ ${selector} .arrowheadPath {
const originalHTML = shareGenerateBtn.innerHTML;
shareGenerateBtn.disabled = true;
shareCopyBtn.disabled = true;
+ if (shareDeleteBtn) shareDeleteBtn.hidden = true;
shareGenerateBtn.textContent = 'Sharing...';
if (shareInviteSection) {
shareInviteSection.hidden = false;
@@ -30669,6 +30678,7 @@ ${selector} .arrowheadPath {
generatedShareSnapshotUrl = url;
shareUrlInput.value = url;
shareCopyBtn.disabled = false;
+ if (shareDeleteBtn) shareDeleteBtn.hidden = !generatedShareSnapshotId || !generatedShareSnapshotDeleteToken;
if (shareInviteSection) {
shareInviteSection.dataset.state = 'active';
}
@@ -30712,6 +30722,24 @@ ${selector} .arrowheadPath {
}
});
+ if (shareDeleteBtn) shareDeleteBtn.addEventListener('click', async function() {
+ const id = generatedShareSnapshotId, deleteToken = generatedShareSnapshotDeleteToken;
+ if (!id || !deleteToken || shareDeleteBtn.disabled) return;
+ shareDeleteBtn.disabled = true;
+ try {
+ const accepted = await showAppToastConfirmation('Delete this stored snapshot? Its link will stop working. Downloaded copies are unaffected.',
+ { title: 'Delete snapshot', confirmLabel: 'Delete', cancelLabel: 'Cancel' });
+ if (!accepted) return;
+ const response = await fetch(getShareApiBaseUrl() + '/api/share/' + encodeURIComponent(id), {
+ method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ deleteToken })
+ });
+ if (!response.ok) throw new Error('Deletion failed');
+ if (generatedShareSnapshotId === id) resetShareSnapshotLink();
+ showAppToast('Snapshot deleted.');
+ } catch (_) { showShareErrorModal('The snapshot could not be deleted. Please try again.'); }
+ finally { shareDeleteBtn.disabled = false; }
+ });
+
shareModalCloseX.addEventListener('click', closeShareModal);
shareModalClose.addEventListener('click', closeShareModal);
shareModal.addEventListener('click', function (e) {
@@ -30928,6 +30956,7 @@ ${selector} .arrowheadPath {
function getCurrentLiveAccessMode() {
if (!liveCollaboration) return getSelectedLiveAccessMode();
+ if (liveCollaboration.encryption && liveCollaboration.socketRole === 'view') return LIVE_SHARE_ACCESS_VIEW;
return normalizeLiveAccessMode(liveCollaboration.accessMode || (liveCollaboration.sessionMap && liveCollaboration.sessionMap.get('accessMode')));
}
@@ -31226,10 +31255,11 @@ ${selector} .arrowheadPath {
function getLiveRoomSocketUrl(roomId, secret, auth) {
const encodedRoom = encodeURIComponent(roomId);
const params = new URLSearchParams();
- params.set('secret', secret);
- if (auth && auth.role) params.set('role', normalizeLiveSocketRole(auth.role));
- if (auth && auth.capability) params.set('cap', auth.capability);
- if (auth && auth.capabilities) {
+ if (auth && auth.encrypted) params.set('v', '2');
+ else params.set('secret', secret);
+ if (auth && !auth.encrypted && auth.role) params.set('role', normalizeLiveSocketRole(auth.role));
+ if (auth && !auth.encrypted && auth.capability) params.set('cap', auth.capability);
+ if (auth && !auth.encrypted && auth.capabilities) {
params.set('hostCap', auth.capabilities.host || '');
params.set('editCap', auth.capabilities.edit || '');
params.set('viewCap', auth.capabilities.view || '');
@@ -31282,6 +31312,13 @@ ${selector} .arrowheadPath {
}
function createLiveRoomConnection(options) {
+ if (options.encryption) {
+ return MarkdownShareLive.connect(Object.assign({}, options, {
+ socketUrl: getLiveRoomSocketUrl(options.roomId, '', { encrypted: true }),
+ relayOrigin: LIVE_RELAY_ORIGIN,
+ reviewRelayOrigin: LIVE_REVIEW_RELAY_ORIGIN
+ }));
+ }
const pendingMessages = [];
const socketUrl = getLiveRoomSocketUrl(options.roomId, options.secret, {
role: options.role,
@@ -31461,7 +31498,7 @@ ${selector} .arrowheadPath {
return (normalized || 'Live Share').slice(0, 120);
}
- function buildLiveInviteUrl(roomId, secret, title, accessMode, capabilities) {
+ function buildLiveInviteUrl(roomId, secret, title, accessMode, capabilities, encryption) {
const isLocal = window.location.origin.includes('localhost') ||
window.location.origin.startsWith('file://') ||
typeof Neutralino !== 'undefined';
@@ -31470,6 +31507,14 @@ ${selector} .arrowheadPath {
? 'https://markdownviewer.pages.dev/'
: window.location.origin + window.location.pathname;
+ if (encryption) {
+ const role = normalizeLiveAccessMode(accessMode);
+ if (!capabilities || !encryption.signing) return '';
+ const params = new URLSearchParams({ live: roomId, v: '2', key: encryption.key,
+ sig: encryption.signing[role], pub: MarkdownShareCrypto.pack(encryption.config, 4096),
+ access: role, role, cap: capabilities[role] });
+ return baseUrl + '#' + params.toString();
+ }
const base = baseUrl + '#live=' + encodeURIComponent(roomId + '.' + secret);
const params = [];
const normalizedAccess = normalizeLiveAccessMode(accessMode);
@@ -31491,6 +31536,16 @@ ${selector} .arrowheadPath {
function parseLiveHash() {
const hash = window.location.hash || '';
if (!hash.startsWith('#live=')) return null;
+ const encryptedParams = new URLSearchParams(hash.slice(1));
+ if (encryptedParams.has('v')) {
+ if (encryptedParams.get('v') !== '2' || hash.length > 8192) throw new Error('Unsupported encrypted invite.');
+ const roomId = encryptedParams.get('live');
+ const role = encryptedParams.get('role');
+ if (!/^room2_[A-Za-z0-9_-]{24}$/.test(roomId || '') || !['view', 'edit'].includes(role)) throw new Error('Invalid encrypted invite.');
+ const config = MarkdownShareCrypto.publicConfig(MarkdownShareCrypto.unpack(encryptedParams.get('pub'), 4096), roomId);
+ return { roomId, secret: '', title: 'Live Share', role, accessMode: role, capability: encryptedParams.get('cap'),
+ encryption: { key: encryptedParams.get('key'), config, signingKey: encryptedParams.get('sig') } };
+ }
const rest = hash.slice('#live='.length);
const paramsIndex = rest.indexOf('&');
const roomSecretPart = decodeURIComponent(paramsIndex === -1 ? rest : rest.slice(0, paramsIndex));
@@ -31550,7 +31605,8 @@ ${selector} .arrowheadPath {
liveCollaboration.secret,
safeTitle,
liveCollaboration.accessMode,
- liveCollaboration.capabilities
+ liveCollaboration.capabilities,
+ liveCollaboration.encryption
);
}
if (liveCollaboration.tabId) {
@@ -31696,9 +31752,10 @@ ${selector} .arrowheadPath {
const liveTab = createTab(typeof markdown === 'string' ? markdown : '', getSafeLiveTitle(liveCollaboration.roomTitle), 'split');
liveTab.kind = 'live-share';
liveTab.temporary = true;
+ liveTab.encryptedSharing = Boolean(liveCollaboration.encryption);
tabs.push(liveTab);
- switchTab(liveTab.id);
liveCollaboration.tabId = liveTab.id;
+ switchTab(liveTab.id);
liveCollaboration.pendingJoinTab = false;
liveCollaboration.removeTabOnLeave = true;
if (liveCollaboration.joinTimeoutId) {
@@ -32301,6 +32358,7 @@ ${selector} .arrowheadPath {
if (!sender || sender === liveCollaboration.localParticipantId) return;
if (message.type === 'sync-request') {
+ if (transport.publishControlState) transport.publishControlState();
transport.publishCurrentState();
transport.publishPresence(liveCollaboration.localCursor);
return;
@@ -32311,6 +32369,12 @@ ${selector} .arrowheadPath {
return;
}
+ if (message.type === 'control-state' && liveCollaboration.encryption && message.role === 'host' && message.update) {
+ try { liveCollaboration.Y.applyUpdate(liveCollaboration.controlDoc, decodeLiveBytes(message.update), LIVE_RELAY_ORIGIN); }
+ catch (_) { setLiveShareStatus('Unable to apply encrypted room controls.', 'error'); }
+ return;
+ }
+
if ((message.type === 'y-update' || message.type === 'sync-state') && message.update) {
const hostParticipantId = liveCollaboration.sessionMap ? liveCollaboration.sessionMap.get('hostId') : null;
if (getCurrentLiveAccessMode() === LIVE_SHARE_ACCESS_VIEW && hostParticipantId && sender !== hostParticipantId) {
@@ -32409,6 +32473,9 @@ ${selector} .arrowheadPath {
if (liveCollaboration.reviewDoc && typeof liveCollaboration.reviewDoc.destroy === 'function') {
liveCollaboration.reviewDoc.destroy();
}
+ if (liveCollaboration.controlDoc && liveCollaboration.controlDoc !== liveCollaboration.ydoc && typeof liveCollaboration.controlDoc.destroy === 'function') {
+ liveCollaboration.controlDoc.destroy();
+ }
} catch (error) {
console.warn('Failed to fully close live session:', error);
}
@@ -32437,7 +32504,7 @@ ${selector} .arrowheadPath {
}
}
- function endLiveSessionForEveryone() {
+ async function endLiveSessionForEveryone() {
if (!liveCollaboration) return;
if (!liveCollaboration.isHost) {
leaveLiveSession({ restoreOriginal: true });
@@ -32452,7 +32519,7 @@ ${selector} .arrowheadPath {
endingSession.sessionMap.set('endedBy', endingSession.localParticipantId);
}, LIVE_EDIT_ORIGIN);
if (endingSession.connection) {
- endingSession.connection.send({ type: 'session-end' });
+ await endingSession.connection.send({ type: 'session-end' });
}
} catch (error) {
console.warn('Failed to broadcast live session end:', error);
@@ -32469,15 +32536,17 @@ ${selector} .arrowheadPath {
async function startLiveSession(options) {
options = options || {};
const isHost = options.isHost !== false;
- const roomId = options.roomId || ('room-' + getRandomBase64Url(9));
- const secret = options.secret || ('secret-' + getRandomBase64Url(32));
+ const roomId = options.roomId || ('room2_' + getRandomBase64Url(18));
+ const encryption = isHost ? await MarkdownShareCrypto.createRoom(roomId) : options.encryption || null;
+ const secret = encryption ? '' : options.secret;
const displayName = getLiveDisplayName({
forceGeneratedName: options.forceGeneratedName === true
});
const modules = await loadLiveCollaborationModules();
const ydoc = new modules.Y.Doc();
const yText = ydoc.getText('markdown');
- const sessionMap = ydoc.getMap('session');
+ const controlDoc = encryption ? new modules.Y.Doc() : ydoc;
+ const sessionMap = controlDoc.getMap('session');
const reviewDoc = new modules.Y.Doc();
const reviewMap = reviewDoc.getMap('reviewThreads');
const hostActiveTab = tabs.find(function(t) { return t.id === activeTabId; });
@@ -32509,7 +32578,7 @@ ${selector} .arrowheadPath {
const liveCapability = isHost
? liveCapabilities.host
: (options.capability || '');
- const inviteUrl = buildLiveInviteUrl(roomId, secret, hostTitle, requestedAccessMode, liveCapabilities);
+ const inviteUrl = buildLiveInviteUrl(roomId, secret, hostTitle, requestedAccessMode, liveCapabilities, encryption);
if (isHost && yText.length === 0) {
yText.insert(0, markdownEditor.value || '');
}
@@ -32553,6 +32622,10 @@ ${selector} .arrowheadPath {
}
const localParticipantId = 'participant-' + getRandomBase64Url(12);
+ if (encryption) {
+ encryption.transport = await MarkdownShareCrypto.liveSession({ roomId, sender: localParticipantId, role: liveRole,
+ key: encryption.key, config: encryption.config, signingKey: isHost ? encryption.signing.host : encryption.signingKey });
+ }
const localColor = getLiveParticipantColor(displayName + '-' + getRandomBase64Url(2));
const localParticipant = {
name: displayName,
@@ -32570,6 +32643,8 @@ ${selector} .arrowheadPath {
liveCollaboration = {
roomId,
secret,
+ encryption,
+ controlDoc,
inviteUrl,
ydoc,
Y: modules.Y,
@@ -32674,6 +32749,8 @@ ${selector} .arrowheadPath {
liveCollaboration.connection = createLiveRoomConnection({
roomId,
secret,
+ encryption,
+ controlDoc,
role: liveCollaboration.socketRole,
capability: liveCollaboration.socketCapability,
capabilities: liveCollaboration.isHost ? liveCollaboration.capabilities : null,
@@ -32718,6 +32795,17 @@ ${selector} .arrowheadPath {
publishLocalParticipant();
liveCollaboration.participantHeartbeatId = setInterval(publishLocalParticipant, 15000);
+ if (encryption) {
+ const activeSession = liveCollaboration;
+ try { await activeSession.connection.ready; }
+ catch (error) {
+ if (liveCollaboration === activeSession) disconnectLiveCollaboration({ restoreOriginal: true });
+ throw error;
+ }
+ if (liveCollaboration !== activeSession) return;
+ if (activeSession.tabId) renderMarkdown({ reason: 'live-encrypted', force: true });
+ }
+
if (liveShareUrlInput) {
liveShareUrlInput.value = inviteUrl;
}
@@ -32787,7 +32875,9 @@ ${selector} .arrowheadPath {
}
async function loadFromLiveHash() {
- const parsed = parseLiveHash();
+ let parsed;
+ try { parsed = parseLiveHash(); }
+ catch (_) { alert('The encrypted invite is incomplete or unsupported.'); return; }
if (!parsed) return;
try {
const originalTabSnapshot = captureLiveTabSnapshot();
@@ -32807,6 +32897,7 @@ ${selector} .arrowheadPath {
accessMode: parsed.accessMode,
role: parsed.role,
capability: parsed.capability,
+ encryption: parsed.encryption,
isHost: false,
forceGeneratedName: true,
originalTabSnapshot,
@@ -32970,6 +33061,14 @@ ${selector} .arrowheadPath {
return;
}
+ if (id.startsWith('s2_')) {
+ const params = new URLSearchParams(ampIdx === -1 ? '' : rest.slice(ampIdx + 1));
+ const decrypted = await MarkdownShareCrypto.open(payload && payload.encrypted, params.get('key'), 'snapshot');
+ if (!decrypted || typeof decrypted.content !== 'string' || typeof decrypted.title !== 'string' ||
+ !['view', 'edit'].includes(decrypted.mode)) throw new Error('Invalid encrypted snapshot.');
+ await openShareSnapshotTab(decrypted.content, decrypted.title, decrypted.mode, true);
+ return;
+ }
const shareMode = payload && payload.mode === 'edit' ? 'edit' : 'view';
await openShareSnapshotTab(
payload && typeof payload.content === 'string' ? payload.content : '',
@@ -32977,8 +33076,7 @@ ${selector} .arrowheadPath {
shareMode
);
} catch (e) {
- console.error('Failed to load stored shared content:', e);
- alert('Network error while loading shared content.');
+ alert(id.startsWith('s2_') ? 'Unable to open the encrypted snapshot. Check the complete link and connection.' : 'Network error while loading shared content.');
}
}
@@ -32990,6 +33088,18 @@ ${selector} .arrowheadPath {
}
if (!hash.startsWith('#share=')) return;
+ if (hash.startsWith('#share=v2.')) {
+ try {
+ const params = new URLSearchParams(hash.slice(1));
+ const encrypted = MarkdownShareCrypto.unpack(params.get('share').slice(3), MAX_SHARE_URL_LENGTH);
+ const payload = await MarkdownShareCrypto.open(encrypted, params.get('key'), 'snapshot');
+ if (!payload || typeof payload.content !== 'string' || typeof payload.title !== 'string' ||
+ !['view', 'edit'].includes(payload.mode)) throw new Error('Invalid encrypted snapshot.');
+ await openShareSnapshotTab(payload.content, payload.title, payload.mode, true);
+ } catch (_) { alert('Unable to open the encrypted snapshot. Check that the complete link is present.'); }
+ return;
+ }
+
// PERF-002: Lazy-load pako when loading shared URL content
if (typeof pako === 'undefined') {
loadScript(CDN.pako).then(function() {
diff --git a/desktop-app/resources/js/sharing-crypto.js b/desktop-app/resources/js/sharing-crypto.js
new file mode 100644
index 00000000..76e68f23
--- /dev/null
+++ b/desktop-app/resources/js/sharing-crypto.js
@@ -0,0 +1,207 @@
+/* Shared browser/Worker protocol. No content keys are held by the service. */
+(function(root) {
+ 'use strict';
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder('utf-8', { fatal: true });
+ const SNAPSHOT_LIMIT = 8 * 1024 * 1024;
+ const MEDIA_LIMIT = 15 * 1024 * 1024;
+ const LIVE_LIMIT = 8 * 1024 * 1024;
+ const MAX_SEQUENCE = 0xffffffff;
+ const TOKEN = /^[A-Za-z0-9_-]{16,160}$/;
+ const TYPES = ['hello', 'presence', 'sync-request', 'sync-state', 'y-update',
+ 'review-sync-request', 'review-sync-state', 'review-update', 'control-state', 'leave', 'session-end'];
+
+ function cryptoApi() {
+ if (!root.crypto || !root.crypto.subtle) throw new Error('Encrypted sharing requires a secure connection and Web Crypto.');
+ return root.crypto;
+ }
+ function encode(bytes) {
+ let binary = '';
+ for (let i = 0; i < bytes.length; i += 0x8000) {
+ binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
+ }
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
+ }
+ function decode(value, limit) {
+ if (typeof value !== 'string' || !value || value.length > Math.ceil(limit * 4 / 3) || !/^[A-Za-z0-9_-]+$/.test(value)) {
+ throw new Error('Invalid encrypted sharing data.');
+ }
+ const binary = atob(value.replace(/-/g, '+').replace(/_/g, '/'));
+ if (binary.length > limit) throw new Error('Encrypted sharing data is too large.');
+ const bytes = Uint8Array.from(binary, ch => ch.charCodeAt(0));
+ if (encode(bytes) !== value) throw new Error('Non-canonical encrypted sharing data.');
+ return bytes;
+ }
+ function random(bytes = 32) { return encode(cryptoApi().getRandomValues(new Uint8Array(bytes))); }
+ function jsonBytes(value, limit) {
+ const bytes = encoder.encode(JSON.stringify(value));
+ if (bytes.length > limit) throw new Error('The content is too large for encrypted sharing.');
+ return bytes;
+ }
+ function exactKeys(value, keys) {
+ return value && typeof value === 'object' && !Array.isArray(value) &&
+ Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
+ }
+ function envelope(value, limit) {
+ if (!exactKeys(value, ['v', 'alg', 'iv', 'ct']) || value.v !== 2 || value.alg !== 'A256GCM' ||
+ typeof value.iv !== 'string' || value.iv.length !== 16 || typeof value.ct !== 'string' ||
+ value.ct.length < 23 || value.ct.length > Math.ceil((limit + 16) * 4 / 3) ||
+ !/^[A-Za-z0-9_-]+$/.test(value.iv) || !/^[A-Za-z0-9_-]+$/.test(value.ct)) {
+ throw new Error('A supported encrypted sharing envelope is required.');
+ }
+ return { v: 2, alg: 'A256GCM', iv: value.iv, ct: value.ct };
+ }
+ async function importAes(key) {
+ const bytes = decode(key, 32);
+ if (bytes.length !== 32) throw new Error('The share key is missing or invalid.');
+ return cryptoApi().subtle.importKey('raw', bytes, 'AES-GCM', false, ['encrypt', 'decrypt']);
+ }
+ async function seal(value, key, purpose, limit = SNAPSHOT_LIMIT) {
+ const bytes = jsonBytes(value, limit);
+ const iv = cryptoApi().getRandomValues(new Uint8Array(12));
+ const ct = await cryptoApi().subtle.encrypt({ name: 'AES-GCM', iv,
+ additionalData: encoder.encode('markdown-viewer:v2:' + purpose), tagLength: 128 },
+ await importAes(key), bytes);
+ return { v: 2, alg: 'A256GCM', iv: encode(iv), ct: encode(new Uint8Array(ct)) };
+ }
+ async function open(value, key, purpose, limit = SNAPSHOT_LIMIT) {
+ const record = envelope(value, limit);
+ try {
+ const bytes = await cryptoApi().subtle.decrypt({ name: 'AES-GCM', iv: decode(record.iv, 12),
+ additionalData: encoder.encode('markdown-viewer:v2:' + purpose), tagLength: 128 },
+ await importAes(key), decode(record.ct, limit + 16));
+ if (bytes.byteLength > limit) throw new Error();
+ return JSON.parse(decoder.decode(bytes));
+ } catch (_) { throw new Error('Unable to decrypt this share. Check that the complete link is present.'); }
+ }
+ function pack(value, limit = SNAPSHOT_LIMIT * 2) { return encode(jsonBytes(value, limit)); }
+ function unpack(value, limit = SNAPSHOT_LIMIT * 2) { return JSON.parse(decoder.decode(decode(value, limit))); }
+ async function hash(value) { return encode(new Uint8Array(await cryptoApi().subtle.digest('SHA-256', encoder.encode(value)))); }
+ function canSend(role, type) {
+ if (!TYPES.includes(type)) return false;
+ if (role === 'host') return true;
+ if (role !== 'edit' && role !== 'view') return false;
+ if (['session-end', 'control-state', 'review-sync-state'].includes(type)) return false;
+ return role === 'edit' || !['sync-state', 'y-update'].includes(type);
+ }
+ async function signingPair() {
+ const pair = await cryptoApi().subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']);
+ return {
+ privateKey: encode(new Uint8Array(await cryptoApi().subtle.exportKey('pkcs8', pair.privateKey))),
+ publicKey: encode(new Uint8Array(await cryptoApi().subtle.exportKey('raw', pair.publicKey)))
+ };
+ }
+ function publicConfig(config, roomId) {
+ if (!exactKeys(config, ['v', 'roomId', 'expiresAt', 'host', 'edit', 'view']) || config.v !== 2 ||
+ config.roomId !== roomId || typeof roomId !== 'string' || !TOKEN.test(roomId) ||
+ !Number.isSafeInteger(config.expiresAt) || config.expiresAt <= 0 ||
+ !['host', 'edit', 'view'].every(role => typeof config[role] === 'string' && decode(config[role], 65).length === 65)) {
+ throw new Error('Invalid encrypted room configuration.');
+ }
+ return { v: 2, roomId, expiresAt: config.expiresAt, host: config.host, edit: config.edit, view: config.view };
+ }
+ async function createRoom(roomId) {
+ const host = await signingPair(), edit = await signingPair(), view = await signingPair();
+ return { key: random(), config: { v: 2, roomId, expiresAt: Date.now() + 86400000, host: host.publicKey, edit: edit.publicKey, view: view.publicKey },
+ signing: { host: host.privateKey, edit: edit.privateKey, view: view.privateKey } };
+ }
+ function header(frame) {
+ return JSON.stringify(['markdown-viewer:live', 2, frame.roomId, frame.sender, frame.session, frame.seq, frame.type, frame.role]);
+ }
+ function signedBytes(frame) { return encoder.encode(JSON.stringify([header(frame), frame.iv, frame.ct])); }
+ function liveFrame(frame) {
+ if (!exactKeys(frame, ['v', 'roomId', 'sender', 'session', 'seq', 'type', 'role', 'iv', 'ct', 'sig']) ||
+ frame.v !== 2 || ![frame.roomId, frame.sender, frame.session].every(value => typeof value === 'string' && TOKEN.test(value)) ||
+ !Number.isSafeInteger(frame.seq) || frame.seq < 1 || frame.seq > MAX_SEQUENCE || !canSend(frame.role, frame.type)) {
+ throw new Error('Invalid encrypted live frame.');
+ }
+ envelope({ v: 2, alg: 'A256GCM', iv: frame.iv, ct: frame.ct }, LIVE_LIMIT);
+ if (typeof frame.sig !== 'string' || decode(frame.sig, 64).length !== 64) throw new Error('Invalid live signature.');
+ return frame;
+ }
+ async function verifyFrame(frame, config) {
+ liveFrame(frame);
+ publicConfig(config, frame.roomId);
+ const key = await cryptoApi().subtle.importKey('raw', decode(config[frame.role], 65), { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
+ if (!await cryptoApi().subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, decode(frame.sig, 64), signedBytes(frame))) {
+ throw new Error('Invalid live signature.');
+ }
+ return true;
+ }
+ async function liveSession(options) {
+ const config = publicConfig(options.config, options.roomId);
+ if (config.expiresAt <= Date.now()) throw new Error('This encrypted room has expired.');
+ const rootBytes = decode(options.key, 32);
+ if (rootBytes.length !== 32 || typeof options.sender !== 'string' || !TOKEN.test(options.sender)) throw new Error('Invalid encrypted room key.');
+ const rootKey = await cryptoApi().subtle.importKey('raw', rootBytes, 'HKDF', false, ['deriveKey']);
+ const signer = await cryptoApi().subtle.importKey('pkcs8', decode(options.signingKey, 256), { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
+ const verifier = await cryptoApi().subtle.importKey('raw', decode(config[options.role], 65), { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
+ const challenge = encoder.encode('markdown-viewer:live:role-check');
+ const proof = await cryptoApi().subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, signer, challenge);
+ if (!await cryptoApi().subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, verifier, proof, challenge)) throw new Error('Invalid live role key.');
+ const session = random(16);
+ let sequence = 0;
+ const received = new Map();
+ async function derive(frame) {
+ return cryptoApi().subtle.deriveKey({ name: 'HKDF', hash: 'SHA-256', salt: decode(frame.session, 32),
+ info: encoder.encode(JSON.stringify(['markdown-viewer:live:v2', frame.roomId, frame.sender])) },
+ rootKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
+ }
+ const ownKey = await derive({ session, roomId: options.roomId, sender: options.sender });
+ return {
+ session,
+ async encrypt(message) {
+ if (!canSend(options.role, message.type) || sequence >= MAX_SEQUENCE) throw new Error('Live message is not permitted.');
+ const frame = { v: 2, roomId: options.roomId, sender: options.sender, session,
+ seq: ++sequence, type: message.type, role: options.role };
+ // A fresh session has a fresh HKDF key. Its counter IV is never reused.
+ const iv = new Uint8Array(12);
+ new DataView(iv.buffer).setUint32(8, frame.seq);
+ const ciphertext = await cryptoApi().subtle.encrypt({ name: 'AES-GCM', iv,
+ additionalData: encoder.encode(header(frame)), tagLength: 128 }, ownKey, jsonBytes(message, LIVE_LIMIT));
+ frame.iv = encode(iv);
+ frame.ct = encode(new Uint8Array(ciphertext));
+ frame.sig = encode(new Uint8Array(await cryptoApi().subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, signer, signedBytes(frame))));
+ // Detect a mismatched/tampered invitation before sending any room data.
+ await verifyFrame(frame, config);
+ return frame;
+ },
+ async decrypt(frame) {
+ if (frame.roomId !== options.roomId) throw new Error('Wrong live room.');
+ await verifyFrame(frame, config);
+ const peer = frame.role + ':' + frame.sender + ':' + frame.session;
+ if (frame.seq <= (received.get(peer) || 0)) throw new Error('Replayed live frame.');
+ if (!received.has(peer) && received.size >= 1024) throw new Error('Too many live sessions.');
+ const plaintext = await cryptoApi().subtle.decrypt({ name: 'AES-GCM', iv: decode(frame.iv, 12),
+ additionalData: encoder.encode(header(frame)), tagLength: 128 }, await derive(frame), decode(frame.ct, LIVE_LIMIT + 16));
+ const message = JSON.parse(decoder.decode(plaintext));
+ if (!message || message.type !== frame.type) throw new Error('Wrong live channel.');
+ received.set(peer, frame.seq);
+ return Object.assign({}, message, { sender: frame.sender, roomId: frame.roomId, role: frame.role });
+ }
+ };
+ }
+ async function readJson(request, limit) {
+ if (!request.body || Number(request.headers.get('Content-Length')) > limit) throw new Error('Request is too large.');
+ const reader = request.body.getReader();
+ const chunks = [];
+ let size = 0;
+ try {
+ while (true) {
+ const part = await reader.read();
+ if (part.done) break;
+ size += part.value.byteLength;
+ if (size > limit) { await reader.cancel(); throw new Error('Request is too large.'); }
+ chunks.push(part.value);
+ }
+ } finally { reader.releaseLock(); }
+ const bytes = new Uint8Array(size);
+ let offset = 0;
+ for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; }
+ return JSON.parse(decoder.decode(bytes));
+ }
+ const api = { SNAPSHOT_LIMIT, MEDIA_LIMIT, LIVE_LIMIT, MAX_SEQUENCE, encode, decode, random, pack, unpack,
+ seal, open, envelope, exactKeys, hash, canSend, publicConfig, createRoom, liveFrame, verifyFrame, liveSession, readJson };
+ root.MarkdownShareCrypto = Object.freeze(api);
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
+})(globalThis);
diff --git a/desktop-app/resources/js/sharing-live.js b/desktop-app/resources/js/sharing-live.js
new file mode 100644
index 00000000..ed50ed6a
--- /dev/null
+++ b/desktop-app/resources/js/sharing-live.js
@@ -0,0 +1,129 @@
+(function(root) {
+ 'use strict';
+ function connect(options) {
+ const C = root.MarkdownShareCrypto;
+ const cryptoSession = options.encryption.transport;
+ const socket = new WebSocket(options.socketUrl);
+ let opened = false, destroyed = false, failed = false;
+ let sendChain = Promise.resolve(), receiveChain = Promise.resolve();
+ let queuedBytes = 0, queuedCount = 0, incomingBytes = 0, incomingCount = 0;
+ const waiting = [];
+ let resolveReady, rejectReady;
+ const ready = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
+ // A rejected connection is also reported through onStatus; callers may await ready.
+ ready.catch(() => {});
+ const timer = setTimeout(() => fail('Encrypted room connection timed out.'), 8000);
+ function fail(message) {
+ if (destroyed || failed) return;
+ failed = true;
+ clearTimeout(timer);
+ waiting.length = 0;
+ options.onStatus(message, 'error');
+ rejectReady(new Error(message));
+ socket.close();
+ }
+ function send(message) {
+ if (destroyed || failed || !C.canSend(options.role, message.type)) return;
+ let copy;
+ try { copy = JSON.stringify(message); } catch (_) { fail('Unable to encrypt a live update.'); return; }
+ if (copy.length > C.LIVE_LIMIT || queuedCount >= 100 || queuedBytes + copy.length > C.LIVE_LIMIT * 2) {
+ fail('Live update queue is full. Rejoin the room to synchronize.'); return;
+ }
+ queuedCount += 1; queuedBytes += copy.length;
+ sendChain = sendChain.then(async () => {
+ if (destroyed || failed) return;
+ const frame = JSON.stringify(await cryptoSession.encrypt(JSON.parse(copy)));
+ if (destroyed || failed) return;
+ if (opened) {
+ if (socket.bufferedAmount > C.LIVE_LIMIT * 2) throw new Error('Backpressure');
+ socket.send(frame);
+ } else {
+ if (waiting.length >= 100 || waiting.reduce((n, value) => n + value.length, 0) + frame.length > C.LIVE_LIMIT * 2) throw new Error('Backpressure');
+ waiting.push(frame);
+ }
+ }).catch(() => fail('Unable to send an encrypted live update. Rejoin the room.'))
+ .finally(() => { queuedCount -= 1; queuedBytes -= copy.length; });
+ return sendChain;
+ }
+ function publishCurrentState() {
+ if (options.canPublishState && !options.canPublishState()) return;
+ send({ type: 'sync-state', update: C.encode(options.Y.encodeStateAsUpdate(options.ydoc)) });
+ }
+ function publishReviewState() {
+ if (!options.reviewDoc || (options.canPublishReviewState && !options.canPublishReviewState())) return;
+ send({ type: 'review-sync-state', update: C.encode(options.Y.encodeStateAsUpdate(options.reviewDoc)) });
+ }
+ function publishControlState() {
+ if (options.role === 'host') send({ type: 'control-state', update: C.encode(options.Y.encodeStateAsUpdate(options.controlDoc)) });
+ }
+ function publishPresence(cursor) {
+ send({ type: 'presence', participant: options.getParticipant(), cursor: cursor || null });
+ }
+ const transport = { publishCurrentState, publishReviewState, publishControlState, publishPresence };
+ const update = (bytes, origin) => {
+ if (origin === options.relayOrigin || (options.canSendUpdate && !options.canSendUpdate())) return;
+ send({ type: 'y-update', update: C.encode(bytes) });
+ };
+ const reviewUpdate = (bytes, origin) => {
+ if (origin === options.reviewRelayOrigin || (options.canSendReviewUpdate && !options.canSendReviewUpdate())) return;
+ send({ type: 'review-update', update: C.encode(bytes) });
+ };
+ options.ydoc.on('update', update);
+ options.reviewDoc.on('update', reviewUpdate);
+ options.controlDoc.on('update', publishControlState);
+ socket.addEventListener('open', () => {
+ if (destroyed || failed) return;
+ socket.send(JSON.stringify({ v: 2, type: 'authenticate', role: options.role,
+ capability: options.capability, capabilities: options.role === 'host' ? options.capabilities : undefined,
+ sender: options.participantId, session: cryptoSession.session, config: options.encryption.config }));
+ });
+ socket.addEventListener('message', event => {
+ if (destroyed || failed) return;
+ if (typeof event.data !== 'string' || event.data.length > Math.ceil((C.LIVE_LIMIT + 16) * 4 / 3) + 4096 ||
+ incomingCount >= 100 || incomingBytes + event.data.length > C.LIVE_LIMIT * 3) {
+ fail('Encrypted room receive limit exceeded.'); return;
+ }
+ incomingCount += 1; incomingBytes += event.data.length;
+ receiveChain = receiveChain.then(async () => {
+ if (destroyed || failed) return;
+ const frame = JSON.parse(event.data);
+ if (!opened && frame.type === 'ready' && frame.v === 2 && Object.keys(frame).length === 2) {
+ opened = true;
+ clearTimeout(timer);
+ waiting.splice(0).forEach(value => socket.send(value));
+ send({ type: 'hello', participant: options.getParticipant() });
+ send({ type: 'sync-request' });
+ send({ type: 'review-sync-request' });
+ publishControlState(); publishCurrentState(); publishReviewState(); publishPresence(options.getCursor());
+ options.onStatus('Connected to encrypted live room', 'connected');
+ resolveReady();
+ return;
+ }
+ if (!opened) throw new Error('Authentication required');
+ const message = await cryptoSession.decrypt(frame);
+ if (destroyed || failed || message.sender === options.participantId) return;
+ options.onMessage(message, transport);
+ }).catch(() => fail('An encrypted live message could not be verified. Rejoin with the complete invite link.'))
+ .finally(() => { incomingCount -= 1; incomingBytes -= event.data.length; });
+ });
+ socket.addEventListener('close', () => fail('Live room disconnected.'));
+ socket.addEventListener('error', () => fail('Encrypted live room connection failed.'));
+ return Object.assign({ ready, send, destroy() {
+ if (destroyed) return;
+ destroyed = true;
+ clearTimeout(timer);
+ rejectReady(new Error('Live room closed.'));
+ options.ydoc.off('update', update);
+ options.reviewDoc.off('update', reviewUpdate);
+ options.controlDoc.off('update', publishControlState);
+ waiting.length = 0;
+ // Complete queued cryptography before the signed leave so sequence numbers stay ordered.
+ sendChain.then(async () => {
+ if (opened && socket.readyState === WebSocket.OPEN) {
+ socket.send(JSON.stringify(await cryptoSession.encrypt({ type: 'leave' })));
+ }
+ }).catch(() => {}).finally(() => socket.close());
+ } }, transport);
+ }
+ root.MarkdownShareLive = Object.freeze({ connect });
+})(globalThis);
diff --git a/desktop-app/resources/js/sharing-media.js b/desktop-app/resources/js/sharing-media.js
new file mode 100644
index 00000000..d8366e59
--- /dev/null
+++ b/desktop-app/resources/js/sharing-media.js
@@ -0,0 +1,139 @@
+/* Client-only encrypted media resolution. Keys are removed before fetch. */
+(function(root) {
+ 'use strict';
+ const states = new WeakMap();
+ const MEDIA_URL = /https?:\/\/[^\s<>"')]+\/api\/(?:image|media)\/e2_[A-Za-z0-9_-]{24}#key=[A-Za-z0-9_-]{43}/g;
+ function reference(value, base) {
+ try {
+ const url = new URL(value, base);
+ const expected = new URL(base);
+ if (url.origin !== expected.origin || url.search || !/^\/api\/(?:image|media)\/e2_[A-Za-z0-9_-]{24}$/.test(url.pathname) ||
+ !/^#key=[A-Za-z0-9_-]{43}$/.test(url.hash)) return null;
+ return { url: url.origin + url.pathname, key: url.hash.slice(5) };
+ } catch (_) { return null; }
+ }
+ function validateDataUrl(dataUrl) {
+ if (typeof dataUrl !== 'string' || dataUrl.length > 14_100_000) throw new Error('Invalid encrypted media.');
+ const match = /^data:((?:image\/(?:avif|bmp|gif|jpeg|png|webp))|(?:video\/(?:mp4|ogg|webm)));base64,([A-Za-z0-9+/]+={0,2})$/.exec(dataUrl);
+ if (!match) throw new Error('Unsupported encrypted media type.');
+ const mime = match[1];
+ const limit = mime.startsWith('video/') ? 10 * 1024 * 1024 : mime === 'image/gif' ? 5 * 1024 * 1024 : 300 * 1024;
+ if (match[2].length > Math.ceil(limit / 3) * 4) throw new Error('Encrypted media is too large.');
+ const bytes = Uint8Array.from(atob(match[2]), ch => ch.charCodeAt(0));
+ const ascii = (a, b) => String.fromCharCode.apply(null, bytes.subarray(a, b));
+ const prefix = values => values.every((value, index) => bytes[index] === value);
+ const valid = {
+ 'image/png': () => prefix([137, 80, 78, 71, 13, 10, 26, 10]),
+ 'image/jpeg': () => prefix([255, 216, 255]),
+ 'image/gif': () => ['GIF87a', 'GIF89a'].includes(ascii(0, 6)),
+ 'image/webp': () => ascii(0, 4) === 'RIFF' && ascii(8, 12) === 'WEBP',
+ 'image/bmp': () => ascii(0, 2) === 'BM',
+ 'image/avif': () => ascii(4, 8) === 'ftyp' && ['avif', 'avis'].includes(ascii(8, 12)),
+ 'video/mp4': () => ascii(4, 8) === 'ftyp',
+ 'video/ogg': () => ascii(0, 4) === 'OggS',
+ 'video/webm': () => prefix([26, 69, 223, 163])
+ }[mime];
+ if (!bytes.length || bytes.length > limit || !valid()) throw new Error('Invalid encrypted media bytes.');
+ return { mime, bytes };
+ }
+ async function decrypt(ref, signal) {
+ const response = await fetch(ref.url, { signal, cache: 'no-store', referrerPolicy: 'no-referrer', credentials: 'omit' });
+ if (!response.ok) throw new Error('Encrypted media is unavailable or expired.');
+ const body = await MarkdownShareCrypto.readJson(response, Math.ceil((MarkdownShareCrypto.MEDIA_LIMIT + 16) * 4 / 3) + 1024);
+ const payload = await MarkdownShareCrypto.open(body.encrypted, ref.key, 'media', MarkdownShareCrypto.MEDIA_LIMIT);
+ if (!MarkdownShareCrypto.exactKeys(payload, ['dataUrl'])) throw new Error('Invalid encrypted media.');
+ const parsed = validateDataUrl(payload.dataUrl);
+ return { dataUrl: payload.dataUrl, mime: parsed.mime, bytes: parsed.bytes };
+ }
+ function prepareHtml(html, options) {
+ const template = document.createElement('template');
+ template.innerHTML = html;
+ const content = template.content;
+ // Template contents are inert: remove remote URLs before nodes enter a live document.
+ content.querySelectorAll('img, video, source').forEach(node => {
+ const value = node.getAttribute('src');
+ const ref = reference(value || '', options.base);
+ if (ref) {
+ node.removeAttribute('src');
+ node.removeAttribute('srcset');
+ node.setAttribute('data-encrypted-media', MarkdownShareCrypto.pack(ref, 2048));
+ }
+ });
+ if (options.privateContent) {
+ content.querySelectorAll('*').forEach(node => {
+ node.removeAttribute('style');
+ node.removeAttribute('srcset');
+ node.removeAttribute('poster');
+ node.removeAttribute('background');
+ node.removeAttribute('ping');
+ for (const name of ['src', 'href', 'xlink:href']) {
+ if (name === 'href' && node.tagName === 'A') continue;
+ const value = node.getAttribute(name);
+ if (value && !value.startsWith('#') && !/^data:(?:image\/(?:png|jpeg|gif|webp|avif|bmp)|video\/(?:mp4|ogg|webm));base64,/.test(value)) {
+ node.removeAttribute(name);
+ if (node.tagName === 'IMG') node.setAttribute('alt', (node.getAttribute('alt') || 'External image') + ' (external resource blocked)');
+ }
+ }
+ });
+ content.querySelectorAll('style, link, iframe, object, embed').forEach(node => node.remove());
+ // Render source locally rather than allowing renderer-specific remote assets/fallbacks.
+ content.querySelectorAll('.mermaid, .plantuml-diagram, .d2-diagram, .graphviz-diagram, .kroki-diagram, .geojson-map, .topojson-map, .stl-viewer, .abc-notation').forEach(node => {
+ const pre = document.createElement('pre');
+ const code = document.createElement('code');
+ let source = node.getAttribute('data-original-code');
+ try { source = source ? decodeURIComponent(source) : node.textContent; } catch (_) { source = node.textContent; }
+ code.textContent = source || '';
+ pre.appendChild(code);
+ const shell = node.closest('.diagram-viewer, .mermaid-container, .geojson-container, .topojson-container, .stl-container, .abc-container');
+ (shell || node).replaceWith(pre);
+ });
+ }
+ return template.innerHTML;
+ }
+ function release(container) {
+ const state = states.get(container);
+ if (!state) return;
+ state.controller.abort();
+ state.urls.forEach(url => URL.revokeObjectURL(url));
+ states.delete(container);
+ }
+ async function resolve(container, options = {}) {
+ release(container);
+ const state = { controller: new AbortController(), urls: [] };
+ states.set(container, state);
+ for (const node of container.querySelectorAll('[data-encrypted-media]')) {
+ if (states.get(container) !== state) return;
+ const timeout = setTimeout(() => state.controller.abort(), 15000);
+ try {
+ const decoded = MarkdownShareCrypto.unpack(node.getAttribute('data-encrypted-media'), 2048);
+ const ref = reference(decoded.url + '#key=' + decoded.key, options.base);
+ if (!ref) throw new Error('Invalid encrypted media reference.');
+ const media = await decrypt(ref, state.controller.signal);
+ if (states.get(container) !== state || !container.contains(node)) return;
+ const url = options.inline ? media.dataUrl : URL.createObjectURL(new Blob([media.bytes], { type: media.mime }));
+ if (!options.inline) state.urls.push(url);
+ node.setAttribute('src', url);
+ if (options.inline) node.removeAttribute('data-encrypted-media');
+ const video = node.tagName === 'VIDEO' ? node : node.closest('video');
+ if (video) video.load();
+ } catch (_) {
+ if (options.inline) throw new Error('An encrypted attachment could not be exported. Check the connection and expiry.');
+ if (states.get(container) === state) node.setAttribute('title', 'Encrypted media is unavailable. Check the complete link and connection.');
+ } finally { clearTimeout(timeout); }
+ }
+ }
+ async function inlineMarkdown(markdown, base) {
+ let output = markdown;
+ for (const url of new Set(markdown.match(MEDIA_URL) || [])) {
+ const ref = reference(url, base);
+ if (!ref) continue;
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), 15000);
+ try { output = output.split(url).join((await decrypt(ref, controller.signal)).dataUrl); }
+ finally { clearTimeout(timer); }
+ }
+ return output;
+ }
+ root.MarkdownShareMedia = Object.freeze({ reference, validateDataUrl, prepareHtml, resolve, release, inlineMarkdown });
+ if (typeof module !== 'undefined' && module.exports) module.exports = root.MarkdownShareMedia;
+})(globalThis);
diff --git a/docs/ENCRYPTED_SHARING.md b/docs/ENCRYPTED_SHARING.md
new file mode 100644
index 00000000..9f2f6be5
--- /dev/null
+++ b/docs/ENCRYPTED_SHARING.md
@@ -0,0 +1,45 @@
+# Encrypted sharing protocol and rollout
+
+New snapshots, managed attachments, and live rooms use version 2 client-side encryption. The implementation uses browser Web Crypto and locally bundled Yjs. No server encryption secret or key-management service is required.
+
+## Snapshot and attachment envelopes
+
+`sharing-crypto.js` generates independent 32-byte random keys. AES-256-GCM encrypts UTF-8 JSON with a random 12-byte IV, a 128-bit tag, and additional authenticated data `markdown-viewer:v2:snapshot` or `markdown-viewer:v2:media`. The exact envelope fields are `v: 2`, `alg: "A256GCM"`, `iv`, and `ct`; binary fields use unpadded canonical base64url. `ct` includes the authentication tag. The content key never appears in the envelope or API request.
+
+Snapshot plaintext is `{content,title,mode}`. Managed-media plaintext is `{dataUrl}` including the MIME type and base64 file bytes. JSON limits are 8 MiB and 15 MiB respectively; encoded request limits allow only the corresponding bounded overhead. The client verifies media signatures, allowed raster/video types, and decoded sizes before upload and after decryption. The server validates envelope shape, lengths, and version without decrypting content.
+
+New ids use 18 random bytes: `s2_` and `e2_`. Storage prefixes are `share-v2:` and `managed-media-v2:`. Snapshot records contain ciphertext, creation time, version, and a deletion-token hash. Media records contain ciphertext, creation/expiry times, and version. Both have a fixed 90-day TTL. Identical media uploads create independent records without refreshing older records.
+
+Complete links carry `key` in the fragment. Inline snapshots pack the encrypted envelope into `#share=v2.&key=...`; stored snapshots use `#id=s2_...&key=...`. Attachments use `/api/image/e2_...#key=...` or `/api/media/e2_...#key=...`. The client removes fragments before requests. Snapshot deletion uses an independent random token; only its SHA-256 hash is stored. The recipient link does not include it.
+
+## Live protocol
+
+Rooms use independent read keys and ECDSA P-256 host/edit/view signing pairs. The public configuration contains exactly `v`, `roomId`, `expiresAt`, `host`, `edit`, `view`. Public keys are raw P-256 points; role private keys are PKCS8 and remain in clients or the role-specific invite fragment. The host key is never included in guest invites. Each role also has an independent connection capability. The first WebSocket frame authenticates that capability over TLS; the relay stores SHA-256 capability hashes, public configuration, expiry, and ended status.
+
+Each connection generates a random 16-byte session salt and derives a session AES-256-GCM key from the read key with HKDF-SHA-256. HKDF info binds room and sender. A 12-byte IV contains a zero prefix and a monotonically increasing 32-bit big-endian sequence counter. A session stops before counter exhaustion. New session salts yield independent keys. Frames contain exactly `v,roomId,sender,session,seq,type,role,iv,ct,sig`.
+
+The canonical authenticated header is the JSON array `["markdown-viewer:live", 2, roomId, sender, session, seq, type, role]`. ECDSA-SHA-256 signs UTF-8 JSON `[headerString, iv, ct]` as defined in `header`/`signedBytes`. Relay and recipients verify signatures against the public role key and reject unauthorized channels. Recipients authenticate AES-GCM before applying payloads and track per-role/sender/session sequence numbers to reject duplicates and reordering. Queues, frame sizes, session histories, sockets, and authentication time are bounded. Processing is serialized per connection.
+
+Document, Review, and host control state use separate channels/documents. Names, cursors, presence, sync content, leave and end events are encrypted. Only host signatures authorize room controls, full Review state, and session end. Viewers may contribute Review changes but cannot publish document changes. Editors cannot publish host controls. Same-role participants share role authority; signatures are not individual identity certificates.
+
+New rooms last at most 24 hours. End persists an ended marker before closing sockets, preventing rejoining. Expiry alarms close sockets and remove authentication state. The relay has no document history. Capability authentication and public signatures do not give the relay content-decryption keys.
+
+## Client rendering and exports
+
+Decrypted content is sanitized before rendering. Inert templates remove external resource attributes before nodes enter the live DOM. Encrypted attachments resolve to local blob URLs, revoked on rerender/disposal. Private shared previews do not invoke remote diagram engines, external maps, or automatic resource loading; advanced blocks display source. Explicit user navigation remains possible. Web Markdown exports inline decrypted attachments. Private HTML exports are static, include inline media, and use a restrictive CSP. Exported files contain readable content.
+
+## Deployment and compatibility
+
+1. Build with `npm ci` and `npm run build`; prepare desktop resources with `node desktop-app/prepare.js` when packaging desktop.
+2. Deploy the updated Durable Object Worker, then the Pages Functions and matching browser assets as one coordinated rollout. Preserve the existing KV namespace, Durable Object binding, and class migration. Include all three `sharing-*.js` files and `assets/yjs.mjs`.
+3. Refresh clients. The service worker cache version changes. Older clients attempting new snapshot/media or room creation receive an upgrade/failure response; they must not retry with an older content format.
+4. Existing snapshot/media reads and provisioned legacy rooms retain their compatibility paths. No migration, bulk deletion, or TTL refresh is performed. Naturally expiring records keep their timeline; legacy records without an expiry are not assigned one implicitly.
+5. Validate synthetic snapshot round trips, encrypted media, host/editor/viewer collaboration, end/expiry behavior, and legacy reads in the target environment before promoting a release. Never log complete links, content keys, role private keys, decrypted content, or raw capabilities.
+
+This change does not re-encrypt existing links. Rollback must retain v2 readers and relay support for already-created encrypted links; reverting to a build without these readers makes them unavailable. Do not restore plaintext creation as an automatic fallback.
+
+## Security scope and verification
+
+Operational metadata remains visible: identifiers, roles, frame types, timing, sizes, and network addresses. Complete links are bearer credentials. Recipients may copy content. Client/application delivery compromise, extensions, exported files, lost keys, and forward secrecy are outside the protection provided here. TLS is still required. There is no claim of an independent cryptographic audit.
+
+Unit tests cover authenticated encryption, malformed envelopes, wrong keys, tampering, context separation, role forgery, replays, record opacity, legacy retention, and bounded requests. Local Miniflare/workerd tests exercise the real relay and storage. Browser tests cover snapshots, media/export, private rendering, and real Yjs collaboration with synthetic local services.
diff --git a/functions/api/image/[[id]].js b/functions/api/image/[[id]].js
index c32de696..1a23d64d 100644
--- a/functions/api/image/[[id]].js
+++ b/functions/api/image/[[id]].js
@@ -1,12 +1,15 @@
+import '../../../sharing-crypto.js';
+const Sharing = globalThis.MarkdownShareCrypto;
const MAX_IMAGE_BYTES = 300 * 1024;
const MAX_GIF_BYTES = 5 * 1024 * 1024;
const MAX_VIDEO_BYTES = 10 * 1024 * 1024;
-const MAX_DATA_URL_CHARS = 14_100_000;
const MEDIA_TTL_SECONDS = 60 * 60 * 24 * 90;
const MEDIA_TTL_MILLISECONDS = MEDIA_TTL_SECONDS * 1000;
const MEDIA_ID_PATTERN = /^[A-Za-z0-9_-]{20,32}$/;
// Keep the established prefix so links created before video support remain valid.
const MEDIA_KEY_PREFIX = "managed-image-v1:";
+const ENCRYPTED_MEDIA_ID = /^e2_[A-Za-z0-9_-]{24}$/;
+const ENCRYPTED_MEDIA_PREFIX = 'managed-media-v2:';
const ALLOWED_ORIGINS = new Set([
"https://markdownviewer.pages.dev",
"null"
@@ -104,27 +107,7 @@ function getMediaByteLimit(mimeType) {
return MAX_IMAGE_BYTES;
}
-function parseMediaDataUrl(value) {
- if (typeof value !== "string" || value.length > MAX_DATA_URL_CHARS) return null;
- const match = value.match(/^data:((?:image\/(?:avif|bmp|gif|jpeg|png|webp))|(?:video\/(?:mp4|ogg|webm)));base64,([A-Za-z0-9+/]+={0,2})$/i);
- if (!match) return null;
- const mimeType = match[1].toLowerCase();
- if (!ALLOWED_MEDIA_TYPES.has(mimeType)) return null;
- const bytes = decodeBase64(match[2]);
- if (!bytes || bytes.byteLength > getMediaByteLimit(mimeType) || !hasValidMediaSignature(mimeType, bytes)) return null;
- return { mimeType, base64: match[2], bytes };
-}
-async function createMediaId(mimeType, bytes) {
- const typeBytes = new TextEncoder().encode(mimeType + "\0");
- const input = new Uint8Array(typeBytes.length + bytes.length);
- input.set(typeBytes, 0);
- input.set(bytes, typeBytes.length);
- const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", input));
- let binary = "";
- digest.forEach(byte => { binary += String.fromCharCode(byte); });
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "").slice(0, 24);
-}
function getPublicMediaUrl(request, id) {
const url = new URL(request.url);
@@ -156,29 +139,25 @@ export async function onRequest({ request, env, params }) {
if (request.method === "POST" && !id) {
const origin = request.headers.get("Origin") || "";
if (!isAllowedOrigin(origin)) return jsonResponse({ error: "origin not allowed" }, { status: 403 }, request);
- const contentLength = Number(request.headers.get("Content-Length"));
- if (Number.isFinite(contentLength) && contentLength > MAX_DATA_URL_CHARS + 1024) {
- return jsonResponse({ error: "media upload is too large" }, { status: 413 }, request);
- }
let body;
try {
- body = await request.json();
+ body = await Sharing.readJson(request, Math.ceil((Sharing.MEDIA_LIMIT + 16) * 4 / 3) + 1024);
} catch (_) {
return jsonResponse({ error: "invalid json" }, { status: 400 }, request);
}
- const media = parseMediaDataUrl(body && body.dataUrl);
- if (!media) return jsonResponse({ error: "invalid or oversized image, GIF, or video" }, { status: 400 }, request);
-
- const mediaId = await createMediaId(media.mimeType, media.bytes);
- const storageKey = MEDIA_KEY_PREFIX + mediaId;
- const existing = await env.SHARE_KV.get(storageKey);
+ if (!Sharing.exactKeys(body, ['encrypted'])) {
+ return jsonResponse({ error: 'Update Markdown Viewer to upload encrypted media.' }, { status: 426 }, request);
+ }
+ let encrypted;
+ try { encrypted = Sharing.envelope(body.encrypted, Sharing.MEDIA_LIMIT); }
+ catch (_) { return jsonResponse({ error: 'invalid encrypted media' }, { status: 400 }, request); }
+ const mediaId = 'e2_' + Sharing.random(18);
+ const storageKey = ENCRYPTED_MEDIA_PREFIX + mediaId;
const createdAt = Date.now();
const expiresAt = createdAt + MEDIA_TTL_MILLISECONDS;
await env.SHARE_KV.put(storageKey, JSON.stringify({
- version: 2,
- mimeType: media.mimeType,
- base64: media.base64,
- size: media.bytes.byteLength,
+ version: 3,
+ encrypted,
createdAt,
expiresAt
}), { expirationTtl: MEDIA_TTL_SECONDS });
@@ -186,10 +165,22 @@ export async function onRequest({ request, env, params }) {
return jsonResponse({
id: mediaId,
url: getPublicMediaUrl(request, mediaId),
- mimeType: media.mimeType,
- size: media.bytes.byteLength,
expiresAt
- }, { status: existing ? 200 : 201 }, request);
+ }, { status: 201 }, request);
+ }
+
+ if ((request.method === 'GET' || request.method === 'HEAD') && ENCRYPTED_MEDIA_ID.test(id)) {
+ const raw = await env.SHARE_KV.get(ENCRYPTED_MEDIA_PREFIX + id);
+ if (!raw) return jsonResponse({ error: 'media not found' }, { status: 404 }, request);
+ try {
+ const record = JSON.parse(raw);
+ if (!Number.isFinite(record.expiresAt) || record.expiresAt <= Date.now()) {
+ return jsonResponse({ error: 'media expired' }, { status: 404 }, request);
+ }
+ const response = jsonResponse({ encrypted: Sharing.envelope(record.encrypted, Sharing.MEDIA_LIMIT) }, {}, request);
+ response.headers.set('Access-Control-Allow-Origin', '*');
+ return request.method === 'HEAD' ? new Response(null, { headers: response.headers }) : response;
+ } catch (_) { return jsonResponse({ error: 'media unavailable' }, { status: 500 }, request); }
}
if ((request.method === "GET" || request.method === "HEAD") && MEDIA_ID_PATTERN.test(id)) {
@@ -213,15 +204,10 @@ export async function onRequest({ request, env, params }) {
return jsonResponse({ error: "media unavailable" }, { status: 500 }, request);
}
const remainingTtl = Math.floor((expiresAt - Date.now()) / 1000);
- if (remainingTtl < 60) {
- await env.SHARE_KV.delete(MEDIA_KEY_PREFIX + id);
+ if (remainingTtl <= 0) {
return jsonResponse({ error: "media expired" }, { status: 404 }, request);
}
- if (!Number.isFinite(storedExpiresAt) || storedExpiresAt <= 0) {
- record.version = 2;
- record.expiresAt = expiresAt;
- await env.SHARE_KV.put(MEDIA_KEY_PREFIX + id, JSON.stringify(record), { expirationTtl: remainingTtl });
- }
+ // Legacy media keeps its original expiry; reading never extends retention.
const bytes = decodeBase64(record.base64);
if (!bytes || bytes.byteLength > getMediaByteLimit(record.mimeType) || !hasValidMediaSignature(record.mimeType, bytes)) {
return jsonResponse({ error: "media unavailable" }, { status: 500 }, request);
diff --git a/functions/api/share/[[id]].js b/functions/api/share/[[id]].js
index 5ec1829a..de5f9487 100644
--- a/functions/api/share/[[id]].js
+++ b/functions/api/share/[[id]].js
@@ -1,9 +1,9 @@
+import '../../../sharing-crypto.js';
+const Sharing = globalThis.MarkdownShareCrypto;
const SHARE_TTL_SECONDS = 60 * 60 * 24 * 90;
-const MAX_CONTENT_CHARS = 8000000;
-const SHARE_ID_LENGTH = 10;
const SHARE_DELETE_TOKEN_BYTES = 18;
-const SHARE_ID_PATTERN = /^[a-z2-9]{6,20}$/;
-const SHARE_ID_ALPHABET = "abcdefghjkmnpqrstuvwxyz23456789";
+const SHARE_ID_PATTERN = /^(?:[a-z2-9]{6,20}|s2_[A-Za-z0-9_-]{24})$/;
+const storageKey = id => id.startsWith('s2_') ? 'share-v2:' + id : id;
const ALLOWED_ORIGINS = new Set([
"https://markdownviewer.pages.dev",
"null"
@@ -68,14 +68,7 @@ function getShareId(params) {
return typeof raw === "string" ? raw : "";
}
-function generateShareId(length = SHARE_ID_LENGTH) {
- const bytes = crypto.getRandomValues(new Uint8Array(length));
- let id = "";
- for (let i = 0; i < bytes.length; i += 1) {
- id += SHARE_ID_ALPHABET[bytes[i] % SHARE_ID_ALPHABET.length];
- }
- return id;
-}
+function generateShareId() { return 's2_' + Sharing.random(18); }
function generateShareDeleteToken() {
const bytes = crypto.getRandomValues(new Uint8Array(SHARE_DELETE_TOKEN_BYTES));
@@ -99,7 +92,7 @@ async function hashDeleteToken(token) {
async function createUniqueShareId(kv) {
for (let attempt = 0; attempt < 5; attempt += 1) {
const id = generateShareId();
- const existing = await kv.get(id);
+ const existing = await kv.get(storageKey(id));
if (!existing) return id;
}
throw new Error("Unable to allocate share id");
@@ -131,31 +124,28 @@ export async function onRequest({ request, env, params }) {
if (request.method === "POST" && !id) {
let body;
try {
- body = await request.json();
+ body = await Sharing.readJson(request, Math.ceil((Sharing.SNAPSHOT_LIMIT + 16) * 4 / 3) + 1024);
} catch (_) {
return jsonResponse({ error: "invalid json" }, { status: 400 }, request);
}
- const content = body && body.content;
- if (typeof content !== "string" || content.length === 0) {
- return jsonResponse({ error: "content required" }, { status: 400 }, request);
- }
- if (content.length > MAX_CONTENT_CHARS) {
- return jsonResponse({ error: "content too large" }, { status: 413 }, request);
+ if (!Sharing.exactKeys(body, ['encrypted'])) {
+ return jsonResponse({ error: 'Update Markdown Viewer to create an encrypted share.' }, { status: 426 }, request);
}
+ let encrypted;
+ try { encrypted = Sharing.envelope(body.encrypted, Sharing.SNAPSHOT_LIMIT); }
+ catch (_) { return jsonResponse({ error: 'invalid encrypted snapshot' }, { status: 400 }, request); }
const shareId = await createUniqueShareId(env.SHARE_KV);
const deleteToken = generateShareDeleteToken();
const record = {
- content,
- mode: sanitizeMode(body.mode),
- title: sanitizeTitle(body.title),
+ version: 2,
+ encrypted,
createdAt: Date.now(),
- size: content.length,
deleteTokenHash: await hashDeleteToken(deleteToken)
};
- await env.SHARE_KV.put(shareId, JSON.stringify(record), {
+ await env.SHARE_KV.put(storageKey(shareId), JSON.stringify(record), {
expirationTtl: SHARE_TTL_SECONDS
});
@@ -173,7 +163,7 @@ export async function onRequest({ request, env, params }) {
let body;
try {
- body = await request.json();
+ body = await Sharing.readJson(request, 1024);
} catch (_) {
return jsonResponse({ error: "invalid json" }, { status: 400 }, request);
}
@@ -183,7 +173,7 @@ export async function onRequest({ request, env, params }) {
return jsonResponse({ error: "delete token required" }, { status: 400 }, request);
}
- const raw = await env.SHARE_KV.get(id);
+ const raw = await env.SHARE_KV.get(storageKey(id));
if (!raw) {
return jsonResponse({ error: "not found" }, { status: 404 }, request);
}
@@ -199,7 +189,7 @@ export async function onRequest({ request, env, params }) {
return jsonResponse({ error: "not found" }, { status: 404 }, request);
}
- await env.SHARE_KV.delete(id);
+ await env.SHARE_KV.delete(storageKey(id));
return jsonResponse({ deleted: true }, {}, request);
}
@@ -208,7 +198,7 @@ export async function onRequest({ request, env, params }) {
return jsonResponse({ error: "invalid id" }, { status: 400 }, request);
}
- const raw = await env.SHARE_KV.get(id);
+ const raw = await env.SHARE_KV.get(storageKey(id));
if (!raw) {
return jsonResponse({ error: "not found" }, { status: 404 }, request);
}
@@ -220,6 +210,11 @@ export async function onRequest({ request, env, params }) {
return jsonResponse({ error: "invalid record" }, { status: 500 }, request);
}
+ if (id.startsWith('s2_')) {
+ try {
+ return jsonResponse({ encrypted: Sharing.envelope(record.encrypted, Sharing.SNAPSHOT_LIMIT) }, {}, request);
+ } catch (_) { return jsonResponse({ error: 'invalid encrypted snapshot' }, { status: 500 }, request); }
+ }
return jsonResponse({
content: record.content || "",
mode: sanitizeMode(record.mode),
diff --git a/functions/live-room/[[room]].js b/functions/live-room/[[room]].js
index 0c5adf7c..91d50d0f 100644
--- a/functions/live-room/[[room]].js
+++ b/functions/live-room/[[room]].js
@@ -37,12 +37,18 @@ export async function onRequest(context) {
if (!roomName || roomName.length > 160) {
return new Response("Invalid live room", { status: 400 });
}
- const secret = new URL(request.url).searchParams.get("secret") || "";
- if (!secret || secret.length > 256) {
+ const url = new URL(request.url);
+ const encrypted = url.searchParams.get('v') === '2';
+ if ((url.searchParams.has('v') && !encrypted) || (encrypted && url.searchParams.size !== 1)) {
+ return new Response('Unsupported live room protocol', { status: 400 });
+ }
+ const secret = url.searchParams.get("secret") || "";
+ if (!encrypted && (!secret || secret.length > 256)) {
return new Response("Invalid live room credentials", { status: 400 });
}
- const id = env.LIVE_ROOMS.idFromName(roomName + ":" + secret);
+ if (encrypted && !/^room2_[A-Za-z0-9_-]{24}$/.test(roomName)) return new Response('Invalid encrypted room', { status: 400 });
+ const id = env.LIVE_ROOMS.idFromName(encrypted ? 'encrypted-v2:' + roomName : roomName + ":" + secret);
return env.LIVE_ROOMS.get(id).fetch(request);
}
diff --git a/index.html b/index.html
index 016f44f9..2c5d8bca 100644
--- a/index.html
+++ b/index.html
@@ -1265,6 +1265,8 @@
Markdown Viewer
Copy
+
Encrypted sharing. Keep the complete link private; it includes the decryption key.