diff --git a/deno.json b/deno.json index 66ab063..af6240d 100644 --- a/deno.json +++ b/deno.json @@ -15,6 +15,8 @@ "build:tiles": "deno run -A tools/build-tiles.ts", "build:icons": "deno run -A tools/make-icons.ts", "dev": "deno task build:ui && deno run -A --watch main.ts", + "relay": "deno run -A relay/main.ts", + "test:sync": "deno run -A relay/test.ts && deno run -A src/sync/mls.test.ts && deno run -A src/sync/mls-transport.test.ts && deno run -A src/sync/atproto.test.ts && deno run -A src/sync/e2e.test.ts", "desktop": "deno task build:ui && deno desktop --hmr -A --include static --include vendor --include tiles --include build main.ts", "build": "deno task build:ui && deno desktop -A --include static --include vendor --include tiles --include build --icon build/icon-1024.png --output dist/Ziran.app main.ts && deno run -A tools/mac-bundle.ts dist/Ziran.app", "build:mac": "deno task build:ui && deno desktop -A --include static --include vendor --include tiles --include build --icon build/icon-1024.png --target aarch64-apple-darwin --output dist/Ziran-arm64.app main.ts && deno run -A tools/mac-bundle.ts dist/Ziran-arm64.app", @@ -22,13 +24,26 @@ "build:win": "deno task build:ui && deno desktop -A --include static --include vendor --include tiles --include build --icon build/icon.ico --target x86_64-pc-windows-msvc --output dist/Ziran.msi main.ts", "build:linux": "deno task build:ui && deno desktop -A --include static --include vendor --include tiles --include build --icon build/icon-1024.png --target x86_64-unknown-linux-gnu --output dist/Ziran.AppImage main.ts", "build:linux-arm": "deno task build:ui && deno desktop -A --include static --include vendor --include tiles --include build --icon build/icon-1024.png --target aarch64-unknown-linux-gnu --output dist/Ziran-arm64.AppImage main.ts", - "build:all": "deno task build:mac && deno task build:mac-intel && deno task build:win && deno task build:linux && deno task build:linux-arm" + "build:all": "deno task build:mac && deno task build:mac-intel && deno task build:win && deno task build:linux && deno task build:linux-arm", + "build:relay": "deno compile -A --target x86_64-unknown-linux-gnu -o deploy/relay/relay-linux-x64 relay/main.ts" }, "lint": { - "exclude": ["vendor/", "static/app.js", "dist/", "node_modules/", ".claude/", ".pi/"] + "exclude": [ + "vendor/", + "static/app.js", + "dist/", + "node_modules/", + ".claude/", + ".pi/" + ] }, "compilerOptions": { - "lib": ["deno.ns", "dom", "dom.iterable", "dom.asynciterable"], + "lib": [ + "deno.ns", + "dom", + "dom.iterable", + "dom.asynciterable" + ], "experimentalDecorators": true, "useDefineForClassFields": false }, @@ -45,6 +60,28 @@ "@atcute/cbor": "npm:@atcute/cbor@^2.3.5", "@atcute/car": "npm:@atcute/car@^6.0.2", "file-type": "npm:file-type@^22.0.1", - "mime": "npm:mime@^4.1.0" + "mime": "npm:mime@^4.1.0", + "ts-mls": "npm:ts-mls@^1.6.2", + "@noble/hashes": "npm:@noble/hashes@^1.8.0", + "@noble/curves/ed25519.js": "npm:@noble/curves@^2.2.0/ed25519.js", + "@noble/curves/nist.js": "npm:@noble/curves@^2.2.0/nist.js", + "@noble/curves/secp256k1.js": "npm:@noble/curves@^2.2.0/secp256k1.js", + "@scure/base": "npm:@scure/base@^2.0.0", + "@atproto/oauth-client-node": "npm:@atproto/oauth-client-node@^0.5.2", + "@atproto/api": "npm:@atproto/api@^0.17.0", + "yjs": "npm:yjs@^13.6.27", + "prosemirror-state": "npm:prosemirror-state@^1.4.3", + "prosemirror-view": "npm:prosemirror-view@^1.34.0", + "prosemirror-model": "npm:prosemirror-model@^1.22.0", + "prosemirror-transform": "npm:prosemirror-transform@^1.9.0", + "prosemirror-collab": "npm:prosemirror-collab@^1.3.1", + "prosemirror-history": "npm:prosemirror-history@^1.4.1", + "prosemirror-keymap": "npm:prosemirror-keymap@^1.2.2", + "prosemirror-commands": "npm:prosemirror-commands@^1.6.0", + "prosemirror-schema-basic": "npm:prosemirror-schema-basic@^1.2.3", + "prosemirror-schema-list": "npm:prosemirror-schema-list@^1.4.1", + "prosemirror-inputrules": "npm:prosemirror-inputrules@^1.4.0", + "prosemirror-dropcursor": "npm:prosemirror-dropcursor@^1.8.1", + "prosemirror-gapcursor": "npm:prosemirror-gapcursor@^1.3.2" } -} +} \ No newline at end of file diff --git a/deno.lock b/deno.lock index 8bb38c1..f4fb8a4 100644 --- a/deno.lock +++ b/deno.lock @@ -6,14 +6,34 @@ "npm:@atcute/car@^6.0.2": "6.0.2_@atcute+cbor@2.3.5__@atcute+cid@2.4.2_@atcute+cid@2.4.2", "npm:@atcute/cbor@^2.3.5": "2.3.5_@atcute+cid@2.4.2", "npm:@atcute/cid@^2.4.2": "2.4.2", + "npm:@atproto/api@0.17": "0.17.7", + "npm:@atproto/oauth-client-node@~0.5.2": "0.5.2", "npm:@dasl/tile-lexicon@2": "2.0.0", "npm:@dasl/tile-loader@2": "2.0.0_@atcute+cbor@2.3.5__@atcute+cid@2.4.2", "npm:@dasl/tile-server@2": "2.0.0", + "npm:@noble/curves@^2.2.0": "2.3.0", + "npm:@noble/hashes@^1.8.0": "1.8.0", + "npm:@scure/base@2": "2.2.0", "npm:@types/express@^5.0.6": "5.0.6", "npm:express@^5.2.1": "5.2.1", "npm:file-type@^22.0.1": "22.0.1", "npm:lit@^3.3.3": "3.3.3", - "npm:mime@^4.1.0": "4.1.0" + "npm:mime@^4.1.0": "4.1.0", + "npm:prosemirror-collab@^1.3.1": "1.3.1", + "npm:prosemirror-commands@^1.6.0": "1.7.2", + "npm:prosemirror-dropcursor@^1.8.1": "1.8.3", + "npm:prosemirror-gapcursor@^1.3.2": "1.4.1", + "npm:prosemirror-history@^1.4.1": "1.5.0", + "npm:prosemirror-inputrules@^1.4.0": "1.5.1", + "npm:prosemirror-keymap@^1.2.2": "1.2.3", + "npm:prosemirror-model@^1.22.0": "1.25.11", + "npm:prosemirror-schema-basic@^1.2.3": "1.2.4", + "npm:prosemirror-schema-list@^1.4.1": "1.5.1", + "npm:prosemirror-state@^1.4.3": "1.4.4", + "npm:prosemirror-transform@^1.9.0": "1.12.0", + "npm:prosemirror-view@^1.34.0": "1.42.2", + "npm:ts-mls@^1.6.2": "1.6.2_@noble+curves@2.3.0", + "npm:yjs@^13.6.27": "13.6.32" }, "jsr": { "@std/internal@1.0.14": { @@ -63,6 +83,263 @@ "@atcute/varint@2.0.1": { "integrity": "sha512-reTtgQ1VPGVRcaTohKVb1tUbpvE7MpguoNS0gjhsxDWFY3t1yqWWvwKK6cJodAmePML36JombvgFwfsblir9Jg==" }, + "@atproto-labs/did-resolver@0.3.7": { + "integrity": "sha512-F3M3U5cU1QSAXX3J5auGEc5N2pgDgpirAjvyDFsytXuyWfrjWfTPd/H+DZrrED7gK+noW0cAWtxxjdX7/cTSVQ==", + "dependencies": [ + "@atproto-labs/fetch", + "@atproto-labs/pipe", + "@atproto-labs/simple-store", + "@atproto-labs/simple-store-memory", + "@atproto/did", + "zod" + ] + }, + "@atproto-labs/fetch-node@0.3.7": { + "integrity": "sha512-WZvjXIeEHGeOPmeckyGiSGeoHhkvzwnKtSrRSxgnHakkNqlKNLmPbUIYOIiJgiYgBSOCbse5NpPLRu3Zdw2+gA==", + "dependencies": [ + "@atproto-labs/fetch", + "@atproto-labs/pipe", + "ipaddr.js@2.4.0", + "undici_v6@npm:undici@6.28.0", + "undici_v7@npm:undici@7.29.0", + "undici_v8@npm:undici@8.10.0" + ] + }, + "@atproto-labs/fetch@0.3.5": { + "integrity": "sha512-iDFZEoNqfL17EVKE+uNzdUD7YF8LWhEhxeBtP6x+PNuAxoXv3ip9vLmB8nNzNxFwVn++FipfDtSiPqmziv1bdA==", + "dependencies": [ + "@atproto-labs/pipe" + ] + }, + "@atproto-labs/handle-resolver-node@0.2.8": { + "integrity": "sha512-S4HR3s15TP85LpBvK2pfmJgXRFEX9rPFpcRfBPeW1Rh2Bj8ixN5VZwOgABz2fHTyMidEZLyFGxc0G5HlVh0aWw==", + "dependencies": [ + "@atproto-labs/fetch-node", + "@atproto-labs/handle-resolver", + "@atproto/did" + ] + }, + "@atproto-labs/handle-resolver@0.4.8": { + "integrity": "sha512-38I+j8Efs+cCgW/NX9RFKKu7qv/AF+FqxlKS8sWjXlNZSHQZqPj5cnKVQhSsJCs4ypPB84iKch8w4/STd33bLg==", + "dependencies": [ + "@atproto-labs/simple-store", + "@atproto-labs/simple-store-memory", + "@atproto/did", + "zod" + ] + }, + "@atproto-labs/identity-resolver@0.4.7": { + "integrity": "sha512-lKDaiBHoxrqTOFdSKAAPpplEtbbFrH4djGjnXJtOSMnZ46k7+G7AkCr1xycGGPOBYfyF54zRnTp0Tt8iu86qWg==", + "dependencies": [ + "@atproto-labs/did-resolver", + "@atproto-labs/handle-resolver" + ] + }, + "@atproto-labs/pipe@0.2.4": { + "integrity": "sha512-n67jCcrC+ouAeO10cWkpPzzLMlDi/lDCU30Us+LGqhOPhT6c4t5ASdBLQi9W3jUQtRzQBt3G9zipF+xKWNvVbw==" + }, + "@atproto-labs/simple-store-memory@0.2.6": { + "integrity": "sha512-DD1v7MEfYF3BAcEpMTTpzfBLWLoI2HuyBhku2YpjHoqPrRrhCtE1alkxfPHjtjJVIjuU/XNU0cF/wB3+5FiKsA==", + "dependencies": [ + "@atproto-labs/simple-store", + "lru-cache" + ] + }, + "@atproto-labs/simple-store@0.5.1": { + "integrity": "sha512-vfvoDhu6ds6BT3Pqe+d2/LBU1WpDRtt69y6zltlfMCoucPm82m1j50/Wb6xTvv+oZ+2j0JyZVXsmXQ12SWYQJg==" + }, + "@atproto/api@0.17.7": { + "integrity": "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw==", + "dependencies": [ + "@atproto/common-web@0.4.21", + "@atproto/lexicon@0.5.2", + "@atproto/syntax@0.4.3", + "@atproto/xrpc@0.7.7", + "await-lock", + "multiformats@9.9.0", + "tlds", + "zod" + ] + }, + "@atproto/common-web@0.4.21": { + "integrity": "sha512-Odq+wdk3YNasGCjjlpl3bCIPvqYHige5DLfMkIffNv/2PI/iIj5ZvAvMvJlJ59OhReKSxtpI0invx5UQPc3+fw==", + "dependencies": [ + "@atproto/lex-data@0.0.15", + "@atproto/lex-json@0.0.16", + "@atproto/syntax@0.5.4", + "zod" + ] + }, + "@atproto/common-web@0.5.8": { + "integrity": "sha512-GiYY2Jgbg1aWe9QGT3TswC/wZNicipBDz0nFo4FrP6XqENEJJRxGKAf/zjvqbesfIQFhHXi1VbSFY7GStB8v8w==", + "dependencies": [ + "@atproto/lex-data@0.1.7", + "@atproto/lex-json@0.1.6", + "@atproto/syntax@0.7.3", + "zod" + ] + }, + "@atproto/did@0.5.4": { + "integrity": "sha512-BlnwQ+obL+4ZA71KH/EzZ3TY+cpSxnLiUI85mjBJIQwvDF/oN2sQA18wIj9jpduviIt2b/cMtlJuzHjzBkfXvw==", + "dependencies": [ + "zod" + ] + }, + "@atproto/jwk-jose@0.2.4": { + "integrity": "sha512-gzDoA0JTwnc0ZJOBLM7WX9xFxtynRS2K1Bofb8epzoMWDQvyvfbPcfkdPrKFM7NXCFUVpGpBnsCB8KFPTf1rCg==", + "dependencies": [ + "@atproto/jwk", + "jose" + ] + }, + "@atproto/jwk-webcrypto@0.3.4": { + "integrity": "sha512-UsFIUozqnRecXPo6HgKV4PW4FqYHxX1V3iAe0rRV6Q2RSfYD8V2mZ89pv8NpvJynnaqJArBQ7HZlcg0F4tRYhA==", + "dependencies": [ + "@atproto/jwk", + "@atproto/jwk-jose", + "zod" + ] + }, + "@atproto/jwk@0.7.4": { + "integrity": "sha512-tq7TUDmNfe1yDfpRgdGQMJdl9TUlJmREQNCag9yg5w8Evu+TOiFiLgiOCbo7X4ouRPSgd1DpOzXbUa8UyKKMZA==", + "dependencies": [ + "multiformats@13.4.2", + "zod" + ] + }, + "@atproto/lex-data@0.0.15": { + "integrity": "sha512-ZsbGiaM5S3CnGrcTMbDGON3bLZzCi/Mx9UvcMREKSRujnF68eHgMiXxJqvykP7+QpOX6tYCK93axZkuJVhtSEw==", + "dependencies": [ + "multiformats@9.9.0", + "tslib", + "uint8arrays", + "unicode-segmenter" + ] + }, + "@atproto/lex-data@0.1.7": { + "integrity": "sha512-kW/dPLqo/WgCLV+XESR4JKwV6c1rZWJGOfuPupZGTjEDAKoBbKXdaEzX9/1vKQYbZ9U3j0DS/n7OFFK7wBugyQ==", + "dependencies": [ + "multiformats@13.4.2", + "tslib", + "unicode-segmenter" + ] + }, + "@atproto/lex-json@0.0.16": { + "integrity": "sha512-IgLgQ0krshVlrIYZ+heTBDbCnM3LmAgWvsaYn5MxvKA3LcBot3PG3ptdO8VOweVZ+WgCLuo39cz9EbUmIbqdtg==", + "dependencies": [ + "@atproto/lex-data@0.0.15", + "tslib" + ] + }, + "@atproto/lex-json@0.1.6": { + "integrity": "sha512-mvrAd0lbyuecIHjyld8QN6MN6CBf4j0GCxLzegsvLh0SvDf+GbYWklkcQqmITL44yFQOwmA/QNIQj0Uvh7+R/g==", + "dependencies": [ + "@atproto/lex-data@0.1.7", + "tslib" + ] + }, + "@atproto/lexicon@0.5.2": { + "integrity": "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ==", + "dependencies": [ + "@atproto/common-web@0.4.21", + "@atproto/syntax@0.4.3", + "iso-datestring-validator", + "multiformats@9.9.0", + "zod" + ] + }, + "@atproto/lexicon@0.6.2": { + "integrity": "sha512-p3Ly6hinVZW0ETuAXZMeUGwuMm3g8HvQMQ41yyEE6AL0hAkfeKFaZKos6BdBrr6CjkpbrDZqE8M+5+QOceysMw==", + "dependencies": [ + "@atproto/common-web@0.4.21", + "@atproto/syntax@0.5.4", + "iso-datestring-validator", + "multiformats@9.9.0", + "zod" + ] + }, + "@atproto/lexicon@0.7.10": { + "integrity": "sha512-HH/3d3z5Qt0JoJ9iOuLP/YT++6lb5wWMQsvgQ2TGfgkbuzDQ6AX6X1H5uJ9luPci4/LaADfpwLZfeqyy/Hjfaw==", + "dependencies": [ + "@atproto/common-web@0.5.8", + "@atproto/syntax@0.7.3", + "multiformats@13.4.2", + "zod" + ] + }, + "@atproto/oauth-client-node@0.5.2": { + "integrity": "sha512-rTGhniY1ukH3CcJqZ6GcV3GLybwY1JxhkC3au/ksnpuHQ9Yttrng67cZRcIw6gQWGWrGzEGOqXX/+Z41i/WyRw==", + "dependencies": [ + "@atproto-labs/did-resolver", + "@atproto-labs/handle-resolver-node", + "@atproto-labs/simple-store", + "@atproto/did", + "@atproto/jwk", + "@atproto/jwk-jose", + "@atproto/jwk-webcrypto", + "@atproto/oauth-client", + "@atproto/oauth-types" + ] + }, + "@atproto/oauth-client@0.8.2": { + "integrity": "sha512-clYhVu/Y0prv3JGu8a4IOnC9jjwwkXFwgySgDXBpgStJ/JNmtH2Sss9XLVq5CDoAZBVLxdb/oT1W1iQJrkoTpA==", + "dependencies": [ + "@atproto-labs/did-resolver", + "@atproto-labs/fetch", + "@atproto-labs/handle-resolver", + "@atproto-labs/identity-resolver", + "@atproto-labs/simple-store", + "@atproto-labs/simple-store-memory", + "@atproto/did", + "@atproto/jwk", + "@atproto/oauth-types", + "@atproto/xrpc@0.8.9", + "core-js", + "multiformats@13.4.2", + "zod" + ] + }, + "@atproto/oauth-types@0.7.5": { + "integrity": "sha512-x75O0HsKB1IGfBikAQrrTX6EL8Rt4Q0+wMcwhZQRGPk/N/WqbYbsW3Powj4R8ZJKSfWCpVfaw32Piu1pTi891Q==", + "dependencies": [ + "@atproto/did", + "@atproto/jwk", + "zod" + ] + }, + "@atproto/syntax@0.4.3": { + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", + "dependencies": [ + "tslib" + ] + }, + "@atproto/syntax@0.5.4": { + "integrity": "sha512-9XJOpMAgsGFxMEIp8nJ8AIWv+krrY1xQMj+wULbbXhQztQV+9aZ0TbG9Jtn3Op2or8Kr6OqyWR4ga9Z189kKDw==", + "dependencies": [ + "tslib" + ] + }, + "@atproto/syntax@0.7.3": { + "integrity": "sha512-7LU8Ra79L5wCqVAZZst2352VMMwkU4KNagVMm6pnURrY/uS7+HrJl3XCuA4V2Gy2dEpkt1r6DBxlsP43/VjDBw==", + "dependencies": [ + "iso-datestring-validator", + "tslib" + ] + }, + "@atproto/xrpc@0.7.7": { + "integrity": "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==", + "dependencies": [ + "@atproto/lexicon@0.6.2", + "zod" + ] + }, + "@atproto/xrpc@0.8.9": { + "integrity": "sha512-3jRiAuHCqYFoT8Nv25xFRmgrfmoo84yBnB5PfVP95JoRP+szIkwHS6mq72ZETYbWmLA1tNyR5QVEHrhHZ1LQhw==", + "dependencies": [ + "@atproto/lexicon@0.7.10", + "zod" + ] + }, "@borewit/text-codec@0.2.2": { "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==" }, @@ -88,6 +365,15 @@ ], "bin": true }, + "@hpke/common@1.10.1": { + "integrity": "sha512-moJwhmtLtuxiUzzNp1jpfBfx8yefKoO9D/RCR9dmwrnc7qjJqId1rEtQz+lSlU5cabX8daToMSx/7HayXOiaFw==" + }, + "@hpke/core@1.8.0": { + "integrity": "sha512-yHuo+2q4HSPUFuxcg87Kiy7QZRk4IeR+cwBB0qW8fHnr71bnRCArM39Cq1bWHBt75gTyeERGD/v1H14yPB2wyw==", + "dependencies": [ + "@hpke/common" + ] + }, "@lit-labs/ssr-dom-shim@1.6.0": { "integrity": "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==" }, @@ -97,6 +383,24 @@ "@lit-labs/ssr-dom-shim" ] }, + "@noble/ciphers@2.1.1": { + "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==" + }, + "@noble/curves@2.3.0": { + "integrity": "sha512-v7cY+4oWYPQszRj6ZFGzTVL7uP2TaLo1xMhWHzYC5wj0ZhOXQ5x+sBre8rF3hi8cAoi0bh1qXoovoOkdFtvqEg==", + "dependencies": [ + "@noble/hashes@2.3.0" + ] + }, + "@noble/hashes@1.8.0": { + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" + }, + "@noble/hashes@2.3.0": { + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==" + }, + "@scure/base@2.2.0": { + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==" + }, "@tokenizer/inflate@0.4.1": { "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", "dependencies": [ @@ -175,6 +479,9 @@ "negotiator" ] }, + "await-lock@2.2.2": { + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==" + }, "body-parser@2.3.0": { "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "dependencies": [ @@ -221,6 +528,10 @@ "cookie@0.7.2": { "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" }, + "core-js@3.50.0": { + "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==", + "scripts": true + }, "core-util-is@1.0.3": { "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, @@ -389,12 +700,24 @@ "ipaddr.js@1.9.1": { "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "ipaddr.js@2.4.0": { + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==" + }, "is-promise@4.0.0": { "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" }, "isarray@1.0.0": { "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "iso-datestring-validator@2.2.2": { + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" + }, + "isomorphic.js@0.2.5": { + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==" + }, + "jose@5.10.0": { + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==" + }, "jszip@3.10.1": { "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dependencies": [ @@ -404,6 +727,13 @@ "setimmediate" ] }, + "lib0@0.2.117": { + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", + "dependencies": [ + "isomorphic.js" + ], + "bin": true + }, "lie@3.3.0": { "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "dependencies": [ @@ -432,6 +762,9 @@ "lit-html" ] }, + "lru-cache@10.4.3": { + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, "math-intrinsics@1.1.0": { "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" }, @@ -457,6 +790,12 @@ "ms@2.1.3": { "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "multiformats@13.4.2": { + "integrity": "sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==" + }, + "multiformats@9.9.0": { + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" + }, "nanoid@5.1.16": { "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", "bin": true @@ -479,6 +818,9 @@ "wrappy" ] }, + "orderedmap@2.1.1": { + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==" + }, "pako@1.0.11": { "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, @@ -491,11 +833,107 @@ "process-nextick-args@2.0.1": { "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "prosemirror-collab@1.3.1": { + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "dependencies": [ + "prosemirror-state" + ] + }, + "prosemirror-commands@1.7.2": { + "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==", + "dependencies": [ + "prosemirror-model", + "prosemirror-state", + "prosemirror-transform" + ] + }, + "prosemirror-dropcursor@1.8.3": { + "integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==", + "dependencies": [ + "prosemirror-state", + "prosemirror-transform", + "prosemirror-view" + ] + }, + "prosemirror-gapcursor@1.4.1": { + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "dependencies": [ + "prosemirror-keymap", + "prosemirror-model", + "prosemirror-state", + "prosemirror-view" + ] + }, + "prosemirror-history@1.5.0": { + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "dependencies": [ + "prosemirror-state", + "prosemirror-transform", + "prosemirror-view", + "rope-sequence" + ] + }, + "prosemirror-inputrules@1.5.1": { + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "dependencies": [ + "prosemirror-state", + "prosemirror-transform" + ] + }, + "prosemirror-keymap@1.2.3": { + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "dependencies": [ + "prosemirror-state", + "w3c-keyname" + ] + }, + "prosemirror-model@1.25.11": { + "integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==", + "dependencies": [ + "orderedmap" + ] + }, + "prosemirror-schema-basic@1.2.4": { + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "dependencies": [ + "prosemirror-model" + ] + }, + "prosemirror-schema-list@1.5.1": { + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "dependencies": [ + "prosemirror-model", + "prosemirror-state", + "prosemirror-transform" + ] + }, + "prosemirror-state@1.4.4": { + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "dependencies": [ + "prosemirror-model", + "prosemirror-transform", + "prosemirror-view" + ] + }, + "prosemirror-transform@1.12.0": { + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", + "dependencies": [ + "prosemirror-model" + ] + }, + "prosemirror-view@1.42.2": { + "integrity": "sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==", + "dependencies": [ + "prosemirror-model", + "prosemirror-state", + "prosemirror-transform" + ] + }, "proxy-addr@2.0.7": { "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dependencies": [ "forwarded", - "ipaddr.js" + "ipaddr.js@1.9.1" ] }, "qs@6.15.3": { @@ -529,6 +967,9 @@ "util-deprecate" ] }, + "rope-sequence@1.3.4": { + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==" + }, "router@2.2.0": { "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dependencies": [ @@ -630,6 +1071,10 @@ "@tokenizer/token" ] }, + "tlds@1.261.0": { + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", + "bin": true + }, "toidentifier@1.0.1": { "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, @@ -641,6 +1086,20 @@ "ieee754" ] }, + "ts-mls@1.6.2_@noble+curves@2.3.0": { + "integrity": "sha512-Li0Ow/IkcV3x2Jk9zi7QaQlqPIegOFUtM1mfBvvJdCysU/byifYFKWpCkKfKpu2wRa9/RH8MaT+rq/JSheUYTQ==", + "dependencies": [ + "@hpke/core", + "@noble/ciphers", + "@noble/curves" + ], + "optionalPeers": [ + "@noble/curves" + ] + }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "type-is@2.1.0": { "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "dependencies": [ @@ -652,9 +1111,27 @@ "uint8array-extras@1.5.0": { "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==" }, + "uint8arrays@3.0.0": { + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", + "dependencies": [ + "multiformats@9.9.0" + ] + }, "undici-types@8.3.0": { "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" }, + "undici@6.28.0": { + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==" + }, + "undici@7.29.0": { + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==" + }, + "undici@8.10.0": { + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==" + }, + "unicode-segmenter@0.14.5": { + "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==" + }, "unpipe@1.0.0": { "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, @@ -664,8 +1141,20 @@ "vary@1.1.2": { "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, + "w3c-keyname@2.2.8": { + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "wrappy@1.0.2": { "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "yjs@13.6.32": { + "integrity": "sha512-lfiJIIC4Xayt5ItynE407ehlE03pCjeOc4hkR4yxxvvNJ4kuiN25B0g+Qp8XagYz361LLL7DCzR5bvFJ81QKtQ==", + "dependencies": [ + "lib0" + ] + }, + "zod@3.25.76": { + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" } }, "workspace": { @@ -674,13 +1163,33 @@ "npm:@atcute/car@^6.0.2", "npm:@atcute/cbor@^2.3.5", "npm:@atcute/cid@^2.4.2", + "npm:@atproto/api@0.17", + "npm:@atproto/oauth-client-node@~0.5.2", "npm:@dasl/tile-lexicon@2", "npm:@dasl/tile-loader@2", "npm:@dasl/tile-server@2", + "npm:@noble/curves@^2.2.0", + "npm:@noble/hashes@^1.8.0", + "npm:@scure/base@2", "npm:express@^5.2.1", "npm:file-type@^22.0.1", "npm:lit@^3.3.3", - "npm:mime@^4.1.0" + "npm:mime@^4.1.0", + "npm:prosemirror-collab@^1.3.1", + "npm:prosemirror-commands@^1.6.0", + "npm:prosemirror-dropcursor@^1.8.1", + "npm:prosemirror-gapcursor@^1.3.2", + "npm:prosemirror-history@^1.4.1", + "npm:prosemirror-inputrules@^1.4.0", + "npm:prosemirror-keymap@^1.2.2", + "npm:prosemirror-model@^1.22.0", + "npm:prosemirror-schema-basic@^1.2.3", + "npm:prosemirror-schema-list@^1.4.1", + "npm:prosemirror-state@^1.4.3", + "npm:prosemirror-transform@^1.9.0", + "npm:prosemirror-view@^1.34.0", + "npm:ts-mls@^1.6.2", + "npm:yjs@^13.6.27" ] } } diff --git a/deploy/relay/.gitignore b/deploy/relay/.gitignore new file mode 100644 index 0000000..4d8deed --- /dev/null +++ b/deploy/relay/.gitignore @@ -0,0 +1,2 @@ +relay-linux-x64 +.env diff --git a/deploy/relay/Dockerfile b/deploy/relay/Dockerfile new file mode 100644 index 0000000..af0751e --- /dev/null +++ b/deploy/relay/Dockerfile @@ -0,0 +1,15 @@ +# The ziran relay ships as a single cross-compiled Deno binary (built by +# `deno task build:relay` before `sm deploy` — see README.md here). The image +# is just a thin glibc base around it; no toolchain, no repo sources. +FROM debian:bookworm-slim +RUN useradd --system --home /data ziran +COPY relay-linux-x64 /usr/local/bin/ziran-relay +RUN chmod +x /usr/local/bin/ziran-relay +USER ziran +ENV ZIRAN_RELAY_HOST=0.0.0.0 \ + ZIRAN_RELAY_PORT=4190 \ + ZIRAN_RELAY_DATA=/data \ + ZIRAN_RELAY_DID=did:web:relay.ziran.space \ + ZIRAN_RELAY_DEV_AUTH=off +EXPOSE 4190 +CMD ["ziran-relay"] diff --git a/deploy/relay/README.md b/deploy/relay/README.md new file mode 100644 index 0000000..5cc8f36 --- /dev/null +++ b/deploy/relay/README.md @@ -0,0 +1,26 @@ +# ziran-relay — sm service + +The hosted relay for Ziran live sessions (`wss://relay.ziran.space/ws`), run +behind the supramundane Caddy front. + +Deploy: + +```sh +deno task build:relay # cross-compiles the relay → deploy/relay/relay-linux-x64 +cd deploy/relay +sm deploy # or: sm deploy --local (serves relay.ziran.bast) +``` + +Notes: + +- **No secrets.** Production auth is atproto service-auth: the relay verifies + each caller's ~60s JWT against their public DID document. Dev HMAC tokens + are disabled via `ZIRAN_RELAY_DEV_AUTH=off` (set in the Dockerfile). If an + override is ever needed, put it in an uncommitted `.env` here — compose + loads it when present. +- The binary is a build artifact (gitignored); rebuild it before every + deploy so the image matches the repo. +- State lives in the shared sm data root (`…/data/ziran-relay`), so the + standard backups cover it. +- `https://relay.ziran.space/.well-known/did.json` is served by the relay + itself, making `did:web:relay.ziran.space` resolvable. diff --git a/deploy/relay/compose.yaml b/deploy/relay/compose.yaml new file mode 100644 index 0000000..7b2c688 --- /dev/null +++ b/deploy/relay/compose.yaml @@ -0,0 +1,20 @@ +services: + ziran-relay: + build: . + restart: unless-stopped + container_name: ziran-relay + # No secrets: production auth is atproto service-auth, verified against + # public DID documents. Overrides (if ever needed) belong in an + # uncommitted .env on the server, never in the repo. + env_file: + - path: .env + required: false + volumes: + - ${SM_DATA_ROOT:-/srv/supramundane/data}/ziran-relay:/data + networks: + - default + +networks: + default: + name: supramundane + external: true diff --git a/deploy/relay/service.json b/deploy/relay/service.json new file mode 100644 index 0000000..d93d516 --- /dev/null +++ b/deploy/relay/service.json @@ -0,0 +1,11 @@ +{ + "name": "ziran-relay", + "type": "node", + "domains": { + "local": ["relay.ziran.bast"], + "prod": ["relay.ziran.space"] + }, + "headers": true, + "upstreamPort": 4190, + "data": true +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 17d3803..f07cf31 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -111,16 +111,17 @@ stored bounds exist. App + document icons are generated by `tools/make-icons.ts` (headless-Chrome rasterization of the brand SVGs; Windows .ico is a PNG-embedded single-image -wrapper). `.tile` / `application/web-tile` association: on macOS, -`tools/mac-bundle.ts` post-processes the built .app (CFBundleDocumentTypes + -UTExportedTypeDeclarations for UTI `ing.dasl.tile`, document icns via -sips/iconutil, lsregister) — wired into the `build`/`build:mac*` tasks; on -Windows/Linux, `src/server/register.ts` registers at first run (HKCU registry -/ XDG mime + desktop entry). File paths passed on the command line open at -boot. Known gap: macOS Finder open-events (Apple Events) don't reach -`Deno.args`; opening a .tile from Finder while ziran runs needs runtime -support Deno desktop doesn't expose yet. `deno task build:all` chains every -platform target. +wrapper). `.tile` / `application/web-tile` association + the `ziran://` URL +scheme: on macOS, `tools/mac-bundle.ts` post-processes the built .app +(CFBundleDocumentTypes + UTExportedTypeDeclarations for UTI `ing.dasl.tile`, +CFBundleURLTypes for `ziran://`, document icns via sips/iconutil, lsregister) +— wired into the `build`/`build:mac*` tasks; on Windows/Linux, +`src/server/register.ts` registers at first run (HKCU registry / XDG mime + +desktop entry + x-scheme-handler). File paths and `ziran://invite/` +links passed on the command line are handled at boot. Known gap: macOS +Finder/browser open-events (Apple Events) don't reach `Deno.args`; opening a +.tile or deep link while ziran runs needs runtime support Deno desktop +doesn't expose yet. `deno task build:all` chains every platform target. Documents and models carry real tile metadata — name, icon (served from the tile's own resources via `/api/docs/:id/icon`, surfaced in recents, models, @@ -128,9 +129,87 @@ window bars, and tabs), model provenance, verified/damaged state. `zn-doc-canvas` is the tile frame (shuttle iframe) with skeleton/damaged states wired to real load/verify. -**Deferred (stubs kept honest in UI):** live sync (CRDT), Atmosphere publish, -accounts/OAuth. The connections panel shows tile facts (CID, verified, model) -and disables the rest with "arrives with sync". +## Starter tiles (tiles/) + +Five collaborative starters, each demonstrating a different sync shape; all +built by `deno task build:tiles` (which first vendors third-party bundles +into the tiles that use them — tiles are self-contained) and seeded per +model id, so new starters reach existing profiles: + +- **note** — solo self-saving text (the original demo). +- **checklist** — idempotent ops over the ordered echo; no CRDT needed. +- **tictactoe** — document-defined roles end to end: manifest declares + `x` (owner/admin), `o` (write, max 1), `watcher` (read, sees players); + the tile enforces turn/legality deterministically (attribution is + runtime-attested), the relay enforces write/caps. +- **livetext** — the tile picks Yjs as its CRDT (vendored ~91 KB); + updates ride the durable log, cursor presence rides the ephemeral lane + (mirror-div rendering, relative positions preserve the local caret). +- **richtext** — ProseMirror (~230 KB vendored) with the ordered log as + collab authority: steps-ops are valid iff their base version matches at + arrival (deterministic accept/ignore under total order; rejected senders + rebase via prosemirror-collab and resend). Toolbar, comments (marks + + synced threads), suggestion mode (insertions marked by an + appendTransaction pass, deletions by keymap; accept/reject in the + sidebar), images as data URLs, remote selections/carets. On first share + (`ready` at version 0) it rebases pending solo steps into the baseline so + the snapshot and log never double-apply. + +Hard-won invariant from these: **MlsGroup serializes every state-advancing +op through one promise chain** — concurrent encrypts would reuse a ratchet +generation (nonce reuse; receivers drop the frames). Rapid per-keystroke +sends surfaced this. + +## Collaboration (E2EE live sessions) + +Full design in `docs/SYNC-DESIGN.md`; research in `docs/SYNC-OPTIONS.md`. + +- **Relay** (`relay/`): standalone dumb Delivery Service — per-document + server-ordered log, ephemeral presence lane, snapshot blob, `inbox/` + invitation mailboxes, KeyPackage directory (stand-in for PDS records), and + the `/i/` share-link landing page. Enforces only write-permission, + membership/caps, and per-role presence visibility from the document's + declared roles; payloads are opaque MLS ciphertext. `deno task relay`, or + the app embeds one on :4190 when `ZIRAN_RELAY` is unset (a second local + instance joins the first's — same-machine collaboration works out of the + box). +- **Crypto** (`src/sync/mls.ts`): ts-mls behind a narrow façade; MLS runs in + the Deno process only. `src/sync/mls-transport.ts` wraps the plaintext + relay client so the relay never sees plaintext; group state persists under + app-data `sync/groups/` (debounced after every ratchet advance). +- **Runtime** (`src/server/sync.ts`): SyncManager owns identity (dev DID in + `sync/identity.json`; `ZIRAN_DID`/`ZIRAN_HANDLE` override — the AT-sign-in + swap point), the shared-doc registry (`sync/docs.json`, localId↔syncId), + live sessions with per-tile replay buffers, invitations, and snapshots: + the tile's own `putData` persistence doubles as the catch-up snapshot + (`ackedSeq` advances on write, snapshot uploads after). +- **Tile protocol** (`vendor/web-tiles/sync.js`): send/onMessage (attributed, + server-ordered, dupes dropped), sendEphemeral/onEphemeral, roster/me. No + membership surface, by construction. The UI bridges frames between tp-data + and the backend (`src/ui/data-protocol.ts` ↔ `/api/docs/:id/sync/*`). +- **UI**: connections panel "Live Session" section (go live, roster, invite + by DID + role), invitation strip in the base window (Accept = download → + verify → open → join), live badge + member count in the doc banner/chip. +- **Tests**: `deno task test:sync` (relay 18, MLS 15, E2EE transport 9, + two-runtime API e2e 20); full-browser two-user flow in the scratchpad + harness (`ziran-sync-ui.mjs`, 13 checks incl. tile-to-tile round-trips). + +- **AT identity** (`src/sync/atproto.ts`, `src/server/atsession.ts`): sign in + with an AT handle via atproto OAuth (loopback client; `/oauth/callback`; + sessions in app-data). Signed in, relay tokens are service-auth JWTs + minted at the user's PDS and verified by the relay against their DID + document (`AtprotoVerifier`; dev HMAC tokens remain accepted unless + `ZIRAN_RELAY_DEV_AUTH=off`); MLS KeyPackages publish to the user's own + repo (`space.ziran.keypackage`) with the relay directory as fallback; + invites take `@handle` or DID. Signed out, a local dev identity keeps + everything working. Tests: `src/sync/atproto.test.ts` (offline, all three + curves + relay door); live PAR against bsky.social verified. + +**Deferred:** kick/role-change UI, presence lanes in tiles, log compaction, +relay reconnect, KeyPackage pools (consumed-once), Atmosphere publish. Known +dev gap: dev-identity KeyPackages are per-run, so invitations to a dev +identity need the invitee's app running since its last restart (PDS records +fix this for signed-in users). Build: `deno bundle --platform browser` → `static/app.js`; CSS linked from `static/index.html`. Vite/package.json remain for browser-only UI iteration diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md new file mode 100644 index 0000000..57ca8cb --- /dev/null +++ b/docs/DEPLOY.md @@ -0,0 +1,54 @@ +# Deploying ziran.space + +Two `sm` services live in this repo, both designed to carry **no secrets**: + +| Service | Directory | Domain | What it is | +|---|---|---|---| +| `ziran-web` | `website/` | ziran.space (+ www) | Static landing page (Caddy file_server) | +| `ziran-relay` | `deploy/relay/` | relay.ziran.space | The collaboration relay (compiled Deno binary) | + +## Website + +```sh +cd website +sm deploy # or --local → https://ziran.bast +``` + +Self-contained single page in `website/site/`; no build step, no fonts, no +trackers. The internal Caddyfile canonicalizes www → apex and redirects +`/i/` share links to the relay, so invite links can be written against +the bare domain. + +## Relay + +```sh +deno task build:relay # cross-compiles → deploy/relay/relay-linux-x64 +cd deploy/relay +sm deploy +``` + +- **No secrets anywhere.** Production auth is atproto **service-auth**: each + caller presents a ~60s JWT minted by their own PDS, and the relay verifies + it against their public DID document. `ZIRAN_RELAY_DEV_AUTH=off` (set in + the Dockerfile) disables the dev HMAC path entirely. If an override is ever + needed, it goes in an uncommitted `deploy/relay/.env` on the operator's + machine — compose loads it only when present. +- The binary is gitignored; rebuild before each deploy. +- Relay state (logs, snapshots, inboxes, keypackages) lands in the shared sm + data root under `ziran-relay/`, covered by the standard backups. +- `did:web:relay.ziran.space` resolves because the relay serves its own + `/.well-known/did.json`. +- Caddy proxies WebSockets transparently; clients connect to + `wss://relay.ziran.space/ws`. + +## Pointing the app at production + +The app defaults to a local embedded relay for development. To use the hosted +relay: + +```sh +ZIRAN_RELAY=wss://relay.ziran.space/ws deno task desktop +``` + +(Making the hosted relay the packaged-build default is a release-time switch — +one constant — deliberately not flipped while everything is pre-release.) diff --git a/docs/SYNC-DESIGN.md b/docs/SYNC-DESIGN.md new file mode 100644 index 0000000..0109ea0 --- /dev/null +++ b/docs/SYNC-DESIGN.md @@ -0,0 +1,254 @@ +# Ziran Collaboration — Design & Build Plan + +Decisions confirmed by Robin, 2026-08-06. Chosen direction: **Option C** +(relay substrate + MLS E2EE). See SYNC-OPTIONS.md for the research behind it. + +## Confirmed decisions + +- **Relay**: one relay at `ziran.space`, single-tenant. Federation and + per-document relay selection are deliberately deferred. +- **Roles**: open-ended. The document/tile defines role names and semantics + and enforces what a role *means*; the runtime + relay enforce only the two + primitives they can: **write-permission per role** and **membership** + (who is in, with which role, caps). A chess tile decides that `opponent` + may move and `watcher` may not by rejecting bad messages — it can, because + attribution is trustworthy. +- **Invitation privacy**: relay-only. No breadcrumb record in the inviter's + PDS (that would leak "X invited someone to something"). Invitations live on + the relay and reach the invitee through their relay inbox + share links. +- **Presence**: per-role. The roster and presence lane are filtered by role — + e.g. a chess watcher does not necessarily see other watchers; the document + declares presence visibility per role. +- **Late-join history**: snapshot-based, and **tiles are not forced to do + anything sync-specific** (see below). +- **Iroh**: adopt later as an optional live-session fast path, not in phase 1. + +## The snapshot story (why tiles aren't forced to snapshot) + +A synced tile already persists its authoritative state through the existing +`store.js` self-storage — the same `putData` it uses for local reload/offline. +The runtime treats that persisted state as the document snapshot: + +1. The tile edits and, as always, persists its state to self-storage. +2. On a membership change (new MLS epoch) — or periodically for compaction — + the runtime captures the current tile bytes, tags them with the log + sequence number they reflect (`atSeq`), encrypts them under the current + group key, and uploads them as the catch-up blob. +3. A new member downloads + decrypts the snapshot, loads it, then replays the + short delta tail (`> atSeq`) from the relay log. + +This works for CRDTs (snapshot = current merge, ops apply on top) and for +ordered-state tiles alike (chess: snapshot = moves so far). No special tile +API, no forced snapshot call — snapshotting is a side effect of normal +persistence. The one case needing full-log replay is a tile that syncs but +**never** persists; for that we fall back to bounded replay from the current +epoch start. tp-sync's contract therefore *encourages* persistence for good +late-join without mandating a snapshot method. + +Under MLS this is not just an optimization but a necessity: forward secrecy +means a new member cannot decrypt pre-join messages, so they must receive a +fresh snapshot encrypted under the current epoch key. The mechanism above is +exactly the RFC 9750-sanctioned pattern. + +## The invariant core (transport-independent) + +### tp-sync — the tile-facing protocol + +Served on tile origins next to `store.js`/`data.js`. The tile imports it and +gets an opaque, attributed, ordered channel — it never learns *how* messages +travel: + +```js +import { sync } from '/.well-known/web-tiles/sync.js'; +sync.send(bytes); // append to the persisted, ordered log +sync.onMessage(({author, role, seq, bytes}) => …); // attributed + ordered +sync.sendEphemeral(bytes); // presence lane; fanned out, never stored +sync.onEphemeral(({author, role, bytes}) => …); +sync.roster(); // [{did, handle, role}], role-filtered +sync.onRoster(list => …); +sync.me(); // {did, handle, role} +``` + +**No membership-mutation surface exists in the tile API, by construction.** +Invitations, role assignment, and kicks live only in the runtime chrome +(the connections panel). Tiles can't grant themselves or others rights. + +### SyncTransport — the runtime seam + +The runtime programs against one interface so the relay (phase 1), MLS +(phase 2), and a possible Iroh fast path or Matrix adapter (later) are all +swappable: + +```ts +interface SyncTransport { + join(docId, opts): Promise; // attach to a document's channel + // Session: append(bytes), onMessage, ephemeral(bytes), onEphemeral, + // roster(), onRoster, snapshot(bytes, atSeq), leave() + invite(docId, inviteeDid, role): Promise; + inbox(): AsyncIterable; // standing subscription for this DID + accept(invitation): Promise<{snapshot, atSeq}>; // download-then-sync + fetchSnapshot(docId): Promise<{bytes, atSeq} | null>; +} +``` + +### Rights: two tiers, enforcement point designed to migrate + +- **Tier 1 (phase 1, ships first): relay-enforced.** The document manifest + declares `sync.roles`; the relay checks every append against the writer's + role and enforces membership/caps. Trusts the relay operator. +- **Tier 2 (phase 3, research-gated): cryptographic.** Ink & Switch Keyhive — + capabilities/membership as CRDTs, revocation without coordination, relay + can neither read nor forge. Pre-production; we keep the role model mappable + onto it and don't ship on it yet. + +MLS (phase 2) sits between: server ACL governs *write*, cryptographic group +membership governs *read* (relay can't decrypt). + +### Invitations + +Relay-only delivery, three surfaces: +- **In-Ziran inbox**: each runtime keeps a standing relay subscription to + `inbox/`; invitations appear in the base window. The inbox is the + rendezvous — a reason the single relay exists. +- **Link**: `https://ziran.space/i/` → landing page → `ziran://invite/ + ` deep link (sibling of the file-type registration already shipped), + with a download path for non-users. +- **Later**: any channel (Germ DM, email) just carries the same link. + +Accept = **download, verify, then sync**: fetch the snapshot blob, verify CIDs +with the existing engine, write into `documents/`, open, join, replay tail. + +## Build status (implemented + verified) + +- **Phase 1a DONE** — `vendor/web-tiles/sync.js` (tp-sync), `src/sync/protocol.ts` + (SyncTransport + relay wire types), `relay/` (the service). 18/18 relay + tests (`deno run -A relay/test.ts`). +- **Phase 2 crypto DONE** — `src/sync/mls.ts` (the ts-mls façade: AES-128-GCM + suite, ratchet-tree-in-Welcome, epoch/forward-secrecy, serialize/restore). + 15/15 (`deno run -A src/sync/mls.test.ts`), incl. 2 MiB payload in ~58 ms. +- **Phase 2 transport DONE** — `src/sync/relay-client.ts` (`RelaySyncTransport`, + the plaintext base) + `src/sync/mls-transport.ts` (`MlsSyncTransport`, + wrapping it with group encryption; `KeyPackageDirectory` seam, in-memory impl + for now / PDS later). 9/9 (`deno run -A src/sync/mls-transport.test.ts`), + proving members exchange decryptable messages, late joiners catch up from an + **encrypted** snapshot, and **the relay's stored log + snapshot are + ciphertext only** — no plaintext leaks. +- **Phase 1b DONE** — the app-facing wiring: `src/server/sync.ts` + (SyncManager: identity, registry, live sessions, replay buffers, + snapshot-on-persist), sync routes in `src/server/api.ts`, the UI bridge + (`src/ui/data-protocol.ts` ziranSync ↔ backend), connections-panel "Live + Session" section, invitation strip in the base window, live badge in the + doc banner + chip count, `ziran://` scheme registration (all three + platforms) + relay landing page, embedded relay for out-of-the-box dev, + and the collaborative checklist tile (idempotent ops over the ordered + echo — no CRDT needed, by design). Verified by `deno task test:sync` + (62 checks: relay 18, MLS 15, transport 9, two-runtime e2e 20) plus a + full-browser two-user harness run (13 checks: tile-to-tile round trips, + UI accept flow, ciphertext-only relay). +- **AT sign-in DONE** — the real identity layer: + - `src/sync/atproto.ts`: handle/DID resolution (backlink-verified), + Multikey, service-auth JWT mint/verify — lifted from matey's SDK-free + client modules, fetch-injectable, offline-tested on all three curves + (`src/sync/atproto.test.ts`, 17 checks). + - Relay door: `AtprotoVerifier` validates service-auth JWTs + (aud = `ZIRAN_RELAY_DID`, default `did:web:relay.ziran.space`; lxm + `space.ziran.sync.connect`) by resolving the issuer's DID document; + `CompositeVerifier` routes by token shape (3 segments → atproto JWT, + 2 → dev HMAC; disable dev with `ZIRAN_RELAY_DEV_AUTH=off`). + - App sign-in: `src/server/atsession.ts` — atproto OAuth loopback client + (`@atproto/oauth-client-node` under Deno; pass `fetch` + + `handleResolver` explicitly or the Node-only SSRF wrapper throws), + sessions persisted in app-data, `/oauth/callback` outside the API key + gate (state-validated), `/api/at/signin|signout`. Verified live: PAR + against bsky.social succeeds and returns a real authorize URL. Signed + in, relay tokens come from `getServiceAuth` at the user's PDS; the + KeyPackage publishes to their repo (`space.ziran.keypackage`, rkey self) + AND the relay (fallback for dev identities); fetch prefers the PDS. + - Identity switching: SyncManager rebuilds the transport on + sign-in/sign-out; docs shared under the previous identity fail rejoin + with a warning (membership is per-DID, by design). + - Invites accept `@handle` or DID (resolution + backlink verification). + - UI: sign-in button + popover in the base banner; signed-in chip shows + the handle with a sign-out popover. + - Untested end-to-end: the human consent + callback exchange (needs a + real sign-in — everything up to and including PAR is verified). +- **Deferred** — kick/role-change UI; concurrent-committer handling (single + admin inviter); high-frequency presence via an exporter-derived epoch key + (currently encrypted as MLS app messages — fine at low volume); log + compaction; relay reconnect/backoff; KeyPackage *pools* (consumed-once + semantics; the single `self` record tolerates reuse by construction, per + the Germ pattern). + +## Phased build (original plan) + +- **Phase 1a — foundation:** tp-sync protocol + host bridge; SyncTransport + interface; the **ziran relay** service (per-doc ordered log over WS, snapshot + blob store, `inbox/` mailboxes, atproto service-auth at the door, + open-ended-role ACL). Relay independently tested. +- **Phase 1b — runtime + UX:** wire SyncTransport → relay; invitation + create/accept UI in the connections panel; `ziran://` scheme + landing + page; upgrade the note tile (and add a second collaborative tile) to prove + it; verify two-context editing + late join. +- **Phase 2 — MLS E2EE:** group encryption over the substrate. + - **Library: `ts-mls`** (`npm:ts-mls@1.6.2`; add `@noble/hashes` + explicitly under Deno). Pure TypeScript, WebCrypto-only, RFC 9420, + interop-tested against OpenMLS via the mlswg harness, and *verified + running under Deno 2.9.2* (created a group, add-committed, joined via + Welcome, round-tripped a 5 MiB payload in ~122 ms with ~145 bytes + overhead). It bundles through `deno desktop` with **no native + artifacts** — the decisive advantage over WASM/FFI/sidecar paths. + Unaudited: isolate it behind a narrow `encrypt/decrypt/commit/welcome` + interface so the engine is swappable, and budget an independent review + before GA. Fallback if the audit gap bites: a self-built wasm-bindgen + wrapper over `mls-rs` + `mls-rs-crypto-webcrypto` (single artifact all + targets) or OpenMLS once `openmls-wasm` graduates. + - **MLS runs host-side only** — in the Deno runtime process. The CEF UI + sees only plaintext + membership events; tile iframes never see keys or + ciphertext framing. + - **Relay = Delivery Service** (RFC 9750 confirms the reduced scope): our + server-ordered log already provides the hard part (total order), so the + DS duties collapse to: KeyPackage directory (delegated to the PDS, see + below), **one-commit-per-epoch** (check the MLS epoch on append, reject + losers), Welcome routed to the invitee's inbox mailbox, ciphertext + fan-out. Smallest reference: OpenMLS's `delivery-service` PoC. + - **KeyPackage-in-PDS (Germ pattern):** publish a ziran NSID record + (`space.ziran.declaration` / a keypackage collection) in each user's own + repo — an anchor key bound to the DID plus MLS KeyPackage bytes signed + by it; fetch an invitee's package by resolving their DID → PDS → + `getRecord`. **Consumed-once is not enforceable** by a reader (they + can't delete another's record), so — exactly as Germ does — the protocol + must tolerate KeyPackage reuse by construction (a published package only + admits a contact into a first-contact group with a delegated agent; + real sessions get fresh per-session keys). Rotation = overwrite the + record; consumers switch keys on seeing a newer repo revision. + - **Late join under MLS** is our snapshot story, and it *is* the RFC 9750 + standard: forward secrecy makes pre-join ciphertext undecryptable by + design, so after the add-commit lands a current member sends the fresh + tile snapshot as an ordinary application message in the new epoch; the + joiner applies snapshot + tail. No forced tile snapshot beyond normal + persistence. Snapshots need no chunking below ~1 GiB (MLS field cap); + 5 MiB is a non-issue per the measurement above. + - **Membership ↔ ACL lockstep:** the relay's Tier-1 ACL (who may write) + and the MLS group (who may read) must be mutated together — an accept + adds both a relay member and an MLS add-commit; a kick removes both. + This is the one genuinely new invariant Phase 2 introduces. +- **Phase 3 — cryptographic rights (Keyhive):** research-gated; only when it + reaches usable maturity. +- **Later:** Iroh live fast path; Matrix adapter (matey) if interop is wanted. + +## Sequencing note + +Option C is "B + MLS": the relay/log/inbox/blob substrate must exist before +any encryption layer, and plaintext-first is testable-first. Phase 1 builds +that substrate correctly, with the seams MLS needs; Phase 2 adds encryption +without changing the substrate's shape. + +## Identity & auth (reused, not reinvented) + +- atproto OAuth (`@atproto/oauth-client-node`, desktop backend flow) for + sign-in. +- Per-connection **service-auth JWT** (`com.atproto.server.getServiceAuth`: + ~60s, signed by the user's DID key) verified by the relay by resolving the + DID document — zero shared secret. Roomy's production pattern. +- matey's SDK-free `@matey/client` identity modules (handle/DID resolution, + verification, multikey/JWS) are directly liftable and transport-agnostic. diff --git a/docs/SYNC-OPTIONS.md b/docs/SYNC-OPTIONS.md index 2185008..091bda3 100644 --- a/docs/SYNC-OPTIONS.md +++ b/docs/SYNC-OPTIONS.md @@ -84,7 +84,7 @@ deltas; a counter sends `{delta: 1}`. the base window (a natural fourth region or a banner). This is why a default relay must exist even for P2P-flavored futures: *the inbox is the rendezvous.* - - **Link**: `https://ziran.ing/i/` → web page that deep-links into + - **Link**: `https://ziran.space/i/` → web page that deep-links into the app (`ziran://invite/` — URL-scheme registration is the sibling of the file-type registration we already do) with a download button for non-users. The link carries the invitation token, so it diff --git a/main.ts b/main.ts index 63c71c1..bd7bc69 100644 --- a/main.ts +++ b/main.ts @@ -5,16 +5,26 @@ // with plain `deno run -A main.ts` and it binds 127.0.0.1:4180 for a regular // browser instead. +import { join } from '@std/path'; import { createHandler } from './src/server/api.ts'; import { ZiranStore } from './src/server/store.ts'; +import { SyncManager } from './src/server/sync.ts'; import { startTileServer } from './src/server/tileserver.ts'; import { hasDesktopWindows, openDocWindow, setupMainWindow } from './src/server/windows.ts'; +import { appDataDir } from './src/server/paths.ts'; const store = new ZiranStore(); await store.load(); store.desktop = hasDesktopWindows(); if (store.desktop) store.openOsWindow = openDocWindow; +const sync = new SyncManager(store); +store.syncSummary = () => sync.summary(); +store.onDocMoved = (oldId, newId, newPath) => sync.moved(oldId, newId, newPath); + +const { AtSession } = await import('./src/server/atsession.ts'); +const at = new AtSession(); + // Launch-time bearer token: 127.0.0.1 is reachable by every local process, // so the API only answers requests carrying this key. It reaches the UI // exclusively through the URLs we navigate our own windows to. @@ -22,7 +32,7 @@ if (store.desktop) store.openOsWindow = openDocWindow; store.apiKey = Deno.env.get('ZIRAN_KEY') ?? [...crypto.getRandomValues(new Uint8Array(32))].map((b) => b.toString(16).padStart(2, '0')).join(''); -const handler = createHandler(store); +const handler = createHandler(store, sync, at); // Bind the main server before ANY other listener: under deno desktop the // runtime probes a free port for DENO_SERVE_ADDRESS and releases it, so a @@ -47,6 +57,37 @@ store.tileServerPort = await startTileServer(); store.broadcast(); console.log(`tile server on http://load.localhost:${store.tileServerPort}/`); +// Collaboration relay. ZIRAN_RELAY points at the hosted relay (ziran.space); +// unset, a local embedded relay makes solo dev work out of the box — and if +// another Ziran instance already runs one on this machine, we join it, which +// gives magical same-machine two-instance collaboration. +const relayUrl = await (async () => { + const configured = Deno.env.get('ZIRAN_RELAY'); + if (configured) return configured; + const port = 4190; + try { + const { startRelay } = await import('./relay/main.ts'); + const { DevVerifier } = await import('./relay/auth.ts'); + await startRelay({ + port, + dataDir: join(appDataDir(), 'relay'), + verifier: new DevVerifier(Deno.env.get('ZIRAN_RELAY_SECRET') ?? 'dev-secret'), + }); + console.log(`embedded relay on ws://127.0.0.1:${port}/ws`); + } catch { + console.log(`joining existing local relay on ws://127.0.0.1:${port}/ws`); + } + return `ws://127.0.0.1:${port}/ws`; +})(); +// AT sign-in restores any previous session before sync connects, so the +// relay handshake uses the real identity when there is one. +try { + await at.init(port); +} catch (err) { + console.warn('AT sign-in unavailable:', (err as Error).message); +} +sync.init(relayUrl, at).catch((err) => console.warn('sync init failed:', (err as Error).message)); + setupMainWindow({ onClosed: (winId) => { if (winId !== 'main') store.closeWindow(winId); @@ -75,9 +116,21 @@ if (store.desktop) { registerFileTypes(); } -// Files passed on the command line (file associations invoke us this way on -// Windows/Linux; macOS open-events are a known gap — see ARCHITECTURE.md). +// Files and ziran:// links passed on the command line (file/URL associations +// invoke us this way on Windows/Linux; macOS open-events are a known gap — +// see ARCHITECTURE.md). for (const arg of Deno.args) { + const invite = arg.match(/^ziran:\/\/invite\/([0-9a-f]+)$/); + if (invite) { + // Accept = download first, verify, then join the live session. + sync.accept(invite[1]!) + .then(async ({ path }) => { + const info = await store.openPath(path); + store.createWindow([info.id]); + }) + .catch((err) => console.warn('could not accept invitation:', (err as Error).message)); + continue; + } if (!arg.endsWith('.tile')) continue; try { const info = await store.openPath(await Deno.realPath(arg)); diff --git a/relay/acl.ts b/relay/acl.ts new file mode 100644 index 0000000..bbca670 --- /dev/null +++ b/relay/acl.ts @@ -0,0 +1,37 @@ +// The relay's two enforceable primitives: write-permission per role, and +// membership (including caps and admin rights). Role *names* and their +// meaning are open-ended and belong to the tile; the relay only checks the +// capabilities the manifest declares. + +import type { Member, RoleSpec, SyncManifest } from '../src/sync/protocol.ts'; + +function spec(manifest: SyncManifest, role: string): RoleSpec { + return manifest.roles[role] ?? {}; +} + +export function canWrite(manifest: SyncManifest, role: string): boolean { + return spec(manifest, role).write === true; +} + +export function canPresence(manifest: SyncManifest, role: string): boolean { + return spec(manifest, role).presence !== false; // presence on by default +} + +export function isAdmin(manifest: SyncManifest, role: string): boolean { + return spec(manifest, role).admin === true; +} + +/** Whether adding another member with `role` would exceed the role's cap. */ +export function capReached(manifest: SyncManifest, members: Member[], role: string): boolean { + const max = spec(manifest, role).max; + if (typeof max !== 'number') return false; + return members.filter((m) => m.role === role).length >= max; +} + +/** The roster `viewer` is allowed to see, honoring per-role `seesRoles`. */ +export function visibleRoster(manifest: SyncManifest, members: Member[], viewer: Member): Member[] { + const sees = spec(manifest, viewer.role).seesRoles; + if (!sees) return members; + const allowed = new Set(sees); + return members.filter((m) => m.did === viewer.did || allowed.has(m.role)); +} diff --git a/relay/auth.ts b/relay/auth.ts new file mode 100644 index 0000000..bdf80c0 --- /dev/null +++ b/relay/auth.ts @@ -0,0 +1,117 @@ +// Who is allowed to connect. The relay verifies a bearer token at the door +// and learns the caller's DID — nothing else. +// +// Two token kinds, routed by shape: +// - AtprotoVerifier: a service-auth JWT (3 segments) minted by the caller's +// PDS (`com.atproto.server.getServiceAuth`), verified by resolving the +// issuer's DID document — zero shared secret. The production door. +// - DevVerifier: an HMAC token (2 segments) for dev identities and tests. +// Disable with ZIRAN_RELAY_DEV_AUTH=off in production. + +import { type DidDocument, resolveDidDocument, verifyServiceAuthJwt } from '../src/sync/atproto.ts'; + +export interface Verifier { + /** Return the caller's DID, or null if the token is invalid/expired. */ + verify(token: string): Promise; +} + +/** The lxm (method binding) ziran service-auth tokens are minted for. */ +export const RELAY_LXM = 'space.ziran.sync.connect'; + +/** Validates atproto service-auth JWTs addressed to this relay's DID. */ +export class AtprotoVerifier implements Verifier { + #cache = new Map(); + + constructor( + private relayDid: string, + private resolve: (did: string) => Promise = (did) => resolveDidDocument(did), + private cacheTtlMs = 5 * 60_000, + ) {} + + async verify(token: string): Promise { + try { + return await verifyServiceAuthJwt(token, { + aud: this.relayDid, + lxm: RELAY_LXM, + resolveDid: async (did) => { + const hit = this.#cache.get(did); + if (hit && Date.now() - hit.at < this.cacheTtlMs) return hit.doc; + const doc = await this.resolve(did); + this.#cache.set(did, { doc, at: Date.now() }); + return doc; + }, + }); + } catch { + return null; + } + } +} + +/** Routes by token shape: 3 segments → atproto JWT, 2 → dev HMAC. */ +export class CompositeVerifier implements Verifier { + constructor(private atproto: AtprotoVerifier, private dev?: DevVerifier) {} + + verify(token: string): Promise { + const segments = token.split('.').length; + if (segments === 3) return this.atproto.verify(token); + if (segments === 2 && this.dev) return this.dev.verify(token); + return Promise.resolve(null); + } +} + +const enc = new TextEncoder(); + +function b64url(bytes: Uint8Array): string { + let s = ''; + for (const b of bytes) s += String.fromCharCode(b); + return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} +function unb64url(s: string): Uint8Array { + const p = s.replace(/-/g, '+').replace(/_/g, '/'); + const bin = atob(p + '='.repeat((4 - (p.length % 4)) % 4)); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; +} + +/** HMAC-signed dev/test tokens: `.`. */ +export class DevVerifier implements Verifier { + #key: Promise; + + constructor(secret: string) { + this.#key = crypto.subtle.importKey( + 'raw', + enc.encode(secret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign', 'verify'], + ); + } + + async mint(did: string, ttlSeconds = 3600): Promise { + const payload = b64url(enc.encode(JSON.stringify({ did, exp: Date.now() + ttlSeconds * 1000 }))); + const sig = new Uint8Array(await crypto.subtle.sign('HMAC', await this.#key, enc.encode(payload).slice())); + return `${payload}.${b64url(sig)}`; + } + + async verify(token: string): Promise { + const dot = token.indexOf('.'); + if (dot < 0) return null; + const payload = token.slice(0, dot); + const sig = token.slice(dot + 1); + let ok = false; + try { + ok = await crypto.subtle.verify('HMAC', await this.#key, unb64url(sig).slice(), enc.encode(payload).slice()); + } catch { + return null; + } + if (!ok) return null; + try { + const { did, exp } = JSON.parse(new TextDecoder().decode(unb64url(payload))); + if (typeof did !== 'string' || typeof exp !== 'number' || Date.now() > exp) return null; + return did; + } catch { + return null; + } + } +} diff --git a/relay/main.ts b/relay/main.ts new file mode 100644 index 0000000..89dc8bf --- /dev/null +++ b/relay/main.ts @@ -0,0 +1,317 @@ +// The ziran relay: a deliberately dumb Delivery Service. Per-document ordered +// logs, an ephemeral presence lane, snapshot blobs for late-join, and per-DID +// invitation inboxes — with the only two enforced primitives being +// write-permission per role and membership. Payloads are opaque (plaintext in +// phase 1; MLS ciphertext in phase 2 changes nothing here). +// +// deno run -A relay/main.ts # binds 127.0.0.1:4190 +// ZIRAN_RELAY_SECRET / ZIRAN_RELAY_DATA / ZIRAN_RELAY_PORT to configure. + +import { canPresence, canWrite, capReached, isAdmin, visibleRoster } from './acl.ts'; +import { AtprotoVerifier, CompositeVerifier, DevVerifier, type Verifier } from './auth.ts'; +import { RelayStore } from './store.ts'; +import type { + ClientFrame, + Invitation, + Member, + ServerFrame, + SyncManifest, +} from '../src/sync/protocol.ts'; + +interface Conn { + ws: WebSocket; + did: string; + handle?: string; + joined: Set; + wantsInbox: boolean; +} + +export interface Relay { + port: number; + shutdown(): void; +} + +export async function startRelay(opts: { + port?: number; + dataDir?: string; + verifier: Verifier; +} & Record): Promise { + const store = new RelayStore(opts.dataDir ?? './relay-data'); + await store.load(); + + const conns = new Set(); + const byDoc = new Map>(); + const byDid = new Map>(); + + const send = (ws: WebSocket, frame: ServerFrame) => { + if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(frame)); + }; + const errFrame = (ws: WebSocket, code: string, message: string, id?: string) => + send(ws, { t: 'err', id, code, message }); + + const rosterFor = (docId: string, viewer: Member): Member[] => { + const doc = store.doc(docId)!; + return visibleRoster(doc.manifest, doc.members, viewer); + }; + + /** Push each joined connection a roster filtered to what it may see. */ + const broadcastRoster = (docId: string) => { + for (const c of byDoc.get(docId) ?? []) { + const me = store.memberOf(docId, c.did); + if (me) send(c.ws, { t: 'roster', docId, roster: rosterFor(docId, me) }); + } + }; + + const handleJoin = async (c: Conn, f: Extract) => { + let doc = store.doc(f.docId); + if (!doc) { + const manifest = (f as { manifest?: SyncManifest }).manifest; + if (!manifest) return errFrame(c.ws, 'no-doc', 'document not found and no manifest to create it'); + doc = await store.createDoc(f.docId, c.did, manifest, c.handle); + } + let me = store.memberOf(f.docId, c.did); + if (!me) { + // Not a member: only a declared public role lets an uninvited caller in. + const pub = doc.manifest.publicRole; + if (!pub) return errFrame(c.ws, 'forbidden', 'not a member of this document'); + me = { did: c.did, handle: c.handle, role: pub }; + await store.addMember(f.docId, me); + } + c.joined.add(f.docId); + (byDoc.get(f.docId) ?? byDoc.set(f.docId, new Set()).get(f.docId)!).add(c); + send(c.ws, { t: 'joined', docId: f.docId, me, roster: rosterFor(f.docId, me) }); + for (const e of store.logSince(f.docId, f.sinceSeq ?? 0)) { + send(c.ws, { t: 'msg', docId: f.docId, seq: e.seq, author: e.author, role: e.role, b64: e.b64 }); + } + broadcastRoster(f.docId); + }; + + const handleAppend = async (c: Conn, f: Extract) => { + const me = store.memberOf(f.docId, c.did); + const doc = store.doc(f.docId); + if (!me || !doc || !c.joined.has(f.docId)) return errFrame(c.ws, 'forbidden', 'not joined', f.id); + if (!canWrite(doc.manifest, me.role)) return errFrame(c.ws, 'forbidden', `role ${me.role} cannot write`, f.id); + const entry = await store.append(f.docId, me.did, me.role, f.b64); + send(c.ws, { t: 'ack', id: f.id, seq: entry.seq }); + // Echo to every joined member (senders apply their own change via onMessage + // too — one code path, total order). + for (const other of byDoc.get(f.docId) ?? []) { + send(other.ws, { t: 'msg', docId: f.docId, seq: entry.seq, author: entry.author, role: entry.role, b64: entry.b64 }); + } + }; + + const handleEphemeral = (c: Conn, f: Extract) => { + const me = store.memberOf(f.docId, c.did); + const doc = store.doc(f.docId); + if (!me || !doc || !c.joined.has(f.docId)) return; + if (!canPresence(doc.manifest, me.role)) return; + for (const other of byDoc.get(f.docId) ?? []) { + if (other === c) continue; + const otherMember = store.memberOf(f.docId, other.did); + // Deliver only if the recipient is allowed to see the sender's role. + if (otherMember && visibleRoster(doc.manifest, [me], otherMember).length > 0) { + send(other.ws, { t: 'eph', docId: f.docId, author: me.did, role: me.role, b64: f.b64 }); + } + } + }; + + const handleSnapshot = async (c: Conn, f: Extract) => { + const me = store.memberOf(f.docId, c.did); + const doc = store.doc(f.docId); + if (!me || !doc) return; + if (!canWrite(doc.manifest, me.role)) return; // only writers snapshot + await store.setSnapshot(f.docId, f.atSeq, f.b64); + }; + + const handleInvite = async (c: Conn, f: Extract) => { + const me = store.memberOf(f.docId, c.did); + const doc = store.doc(f.docId); + if (!me || !doc) return errFrame(c.ws, 'forbidden', 'not a member', f.id); + if (!isAdmin(doc.manifest, me.role)) return errFrame(c.ws, 'forbidden', 'not allowed to invite', f.id); + if (!doc.manifest.roles[f.role]) return errFrame(c.ws, 'bad-role', `unknown role ${f.role}`, f.id); + if (capReached(doc.manifest, doc.members, f.role)) return errFrame(c.ws, 'cap', `role ${f.role} is full`, f.id); + const inv: Invitation = { + token: crypto.randomUUID().replace(/-/g, ''), + docId: f.docId, + title: f.title ?? f.docId, + inviterDid: me.did, + inviterHandle: me.handle, + inviteeDid: f.inviteeDid, + role: f.role, + createdAt: Date.now(), + payload: f.payload, // opaque MLS Welcome under E2EE; relay never reads it + }; + await store.putInvite(inv); + send(c.ws, { t: 'invited', id: f.id, invitation: inv }); + // Deliver to the invitee's live inbox subscribers, if any. + for (const ic of byDid.get(f.inviteeDid) ?? []) { + if (ic.wantsInbox) send(ic.ws, { t: 'inbox', invitation: inv }); + } + }; + + const handleAccept = async (c: Conn, f: Extract) => { + const inv = store.invite(f.token); + if (!inv) return errFrame(c.ws, 'no-invite', 'invitation not found', f.id); + if (inv.inviteeDid !== c.did) return errFrame(c.ws, 'forbidden', 'invitation is for another identity', f.id); + if (capReached(store.doc(inv.docId)!.manifest, store.doc(inv.docId)!.members, inv.role)) { + return errFrame(c.ws, 'cap', 'role filled before you accepted', f.id); + } + await store.addMember(inv.docId, { did: c.did, handle: c.handle, role: inv.role }); + const snap = store.snapshot(inv.docId); + send(c.ws, { + t: 'accepted', + id: f.id, + result: { docId: inv.docId, role: inv.role, atSeq: snap?.atSeq ?? 0, snapshotB64: snap?.b64 ?? null }, + }); + broadcastRoster(inv.docId); + }; + + const onFrame = async (c: Conn, raw: string) => { + let f: ClientFrame; + try { + f = JSON.parse(raw); + } catch { + return errFrame(c.ws, 'bad-frame', 'invalid JSON'); + } + switch (f.t) { + case 'join': return handleJoin(c, f); + case 'leave': { + c.joined.delete(f.docId); + byDoc.get(f.docId)?.delete(c); + return; + } + case 'append': return handleAppend(c, f); + case 'ephemeral': return handleEphemeral(c, f); + case 'snapshot': return handleSnapshot(c, f); + case 'invite': return handleInvite(c, f); + case 'accept': return handleAccept(c, f); + case 'sub-inbox': { + c.wantsInbox = true; + for (const inv of store.inboxFor(c.did)) send(c.ws, { t: 'inbox', invitation: inv }); + return; + } + case 'pub-kp': + await store.putKeyPackage(c.did, f.b64); + return; + case 'get-kp': + send(c.ws, { t: 'kp', id: f.id, b64: store.keyPackage(f.did) ?? null }); + return; + case 'get-invite': { + const inv = store.invite(f.token); + // Only the named invitee may fetch the invitation (it carries the + // MLS Welcome for their key). + send(c.ws, { t: 'invite-info', id: f.id, invitation: inv && inv.inviteeDid === c.did ? inv : null }); + return; + } + } + }; + + // Share-link landing: a human-readable page that deep-links into the app. + // It shows only what the link holder already knows (title, inviter, role) — + // the MLS Welcome is NOT here; the app fetches it over the authenticated + // socket, where only the named invitee can. + const landingPage = (token: string): Response => { + const inv = store.invite(token); + const esc = (s: string) => s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]!)); + const body = inv + ? `

You’re invited to “${esc(inv.title)}”

+

${esc(inv.inviterHandle ?? inv.inviterDid)} invited you to collaborate as ${esc(inv.role)}.

+

Open in Ziran

+

Nothing happens without you: opening the invitation downloads the document + to your machine first, and only then joins the live session.

+

No Ziran yet? Get it here, then open this link again.

` + : `

Invitation not found

+

This invitation may have been withdrawn, or the link is incomplete.

`; + return new Response( + ` + Ziran invitation + +
Z
${body}`, + { headers: { 'content-type': 'text/html; charset=utf-8' } }, + ); + }; + + const handler = async (req: Request): Promise => { + const url = new URL(req.url); + if (url.pathname === '/health') return new Response('ok'); + // did:web identity: the DID names this relay as a service-auth audience; + // serving its document makes the DID resolvable by anyone who checks. + if (url.pathname === '/.well-known/did.json') { + const did = Deno.env.get('ZIRAN_RELAY_DID') ?? 'did:web:relay.ziran.space'; + return new Response( + JSON.stringify({ + '@context': ['https://www.w3.org/ns/did/v1'], + id: did, + service: [{ + id: '#ziran_relay', + type: 'ZiranRelay', + serviceEndpoint: `https://${did.replace(/^did:web:/, '')}`, + }], + }), + { headers: { 'content-type': 'application/json' } }, + ); + } + const invitePage = url.pathname.match(/^\/i\/([0-9a-f]+)$/); + if (invitePage) return landingPage(invitePage[1]!); + if (url.pathname !== '/ws' || req.headers.get('upgrade') !== 'websocket') { + return new Response('ziran relay', { status: 200 }); + } + const did = await opts.verifier.verify(url.searchParams.get('token') ?? ''); + if (!did) return new Response('unauthorized', { status: 401 }); + const handle = url.searchParams.get('handle') ?? undefined; + const { socket, response } = Deno.upgradeWebSocket(req); + const c: Conn = { ws: socket, did, handle, joined: new Set(), wantsInbox: false }; + socket.addEventListener('open', () => { + conns.add(c); + (byDid.get(did) ?? byDid.set(did, new Set()).get(did)!).add(c); + }); + socket.addEventListener('message', (ev) => { + if (typeof ev.data === 'string') onFrame(c, ev.data).catch((e) => errFrame(socket, 'internal', String(e))); + }); + socket.addEventListener('close', () => { + conns.delete(c); + byDid.get(did)?.delete(c); + for (const docId of c.joined) { + byDoc.get(docId)?.delete(c); + broadcastRoster(docId); + } + }); + return response; + }; + + // Loopback by default (dev); containers set ZIRAN_RELAY_HOST=0.0.0.0 — the + // front proxy is the only thing exposed to the outside either way. + const hostname = (opts.hostname as string | undefined) ?? Deno.env.get('ZIRAN_RELAY_HOST') ?? '127.0.0.1'; + const server = Deno.serve({ hostname, port: opts.port ?? 4190, onListen: () => {} }, handler); + return { + port: (server.addr as Deno.NetAddr).port, + shutdown: () => server.shutdown(), + }; +} + +/** The standard door: atproto service-auth JWTs, plus dev HMAC tokens unless + ZIRAN_RELAY_DEV_AUTH=off. ZIRAN_RELAY_DID is the audience tokens must + name (default did:web:relay.ziran.space). */ +export function standardVerifier(): Verifier { + const relayDid = Deno.env.get('ZIRAN_RELAY_DID') ?? 'did:web:relay.ziran.space'; + const dev = Deno.env.get('ZIRAN_RELAY_DEV_AUTH') === 'off' + ? undefined + : new DevVerifier(Deno.env.get('ZIRAN_RELAY_SECRET') ?? 'dev-secret'); + return new CompositeVerifier(new AtprotoVerifier(relayDid), dev); +} + +if (import.meta.main) { + const relay = await startRelay({ + port: Number(Deno.env.get('ZIRAN_RELAY_PORT') ?? 4190), + dataDir: Deno.env.get('ZIRAN_RELAY_DATA') ?? './relay-data', + verifier: standardVerifier(), + }); + console.log(`ziran relay on ws://127.0.0.1:${relay.port}/ws`); +} diff --git a/relay/store.ts b/relay/store.ts new file mode 100644 index 0000000..899c74e --- /dev/null +++ b/relay/store.ts @@ -0,0 +1,208 @@ +// Durable state for the relay: per-document ordered logs, membership, the +// latest snapshot blob, and per-DID invitation inboxes. Filesystem-backed +// (append-only JSONL logs + JSON sidecars + blob files) — simple, inspectable, +// and enough for a single-tenant relay. Everything is indexed in memory and +// written through on mutation. + +import { join } from '@std/path'; +import type { Invitation, Member, SyncManifest } from '../src/sync/protocol.ts'; + +export interface LogEntry { + seq: number; + author: string; + role: string; + b64: string; +} + +interface DocState { + id: string; + manifest: SyncManifest; + owner: string; + members: Member[]; + log: LogEntry[]; + snapshot?: { atSeq: number; b64: string }; +} + +export class RelayStore { + #root: string; + #docs = new Map(); + #inbox = new Map(); + #invites = new Map(); + + #keyPackages = new Map(); + + constructor(root: string) { + this.#root = root; + } + + async load(): Promise { + await Deno.mkdir(join(this.#root, 'docs'), { recursive: true }); + await Deno.mkdir(join(this.#root, 'inbox'), { recursive: true }); + await Deno.mkdir(join(this.#root, 'invites'), { recursive: true }); + await Deno.mkdir(join(this.#root, 'keypackages'), { recursive: true }); + for await (const entry of Deno.readDir(join(this.#root, 'keypackages'))) { + if (!entry.isFile || !entry.name.endsWith('.b64')) continue; + const did = decodeURIComponent(entry.name.replace(/\.b64$/, '')); + this.#keyPackages.set(did, await Deno.readTextFile(join(this.#root, 'keypackages', entry.name))); + } + for await (const entry of Deno.readDir(join(this.#root, 'docs'))) { + if (!entry.isDirectory) continue; + try { + this.#docs.set(entry.name, await this.#readDoc(entry.name)); + } catch (err) { + console.warn(`relay: skipping unreadable doc ${entry.name}:`, (err as Error).message); + } + } + for await (const entry of Deno.readDir(join(this.#root, 'inbox'))) { + if (!entry.isFile || !entry.name.endsWith('.jsonl')) continue; + const did = decodeURIComponent(entry.name.replace(/\.jsonl$/, '')); + this.#inbox.set(did, await this.#readLines(join(this.#root, 'inbox', entry.name))); + } + for await (const entry of Deno.readDir(join(this.#root, 'invites'))) { + if (!entry.isFile || !entry.name.endsWith('.json')) continue; + const inv = JSON.parse(await Deno.readTextFile(join(this.#root, 'invites', entry.name))) as Invitation; + this.#invites.set(inv.token, inv); + } + } + + hasDoc(id: string): boolean { + return this.#docs.has(id); + } + + doc(id: string): DocState | undefined { + return this.#docs.get(id); + } + + async createDoc(id: string, owner: string, manifest: SyncManifest, ownerHandle?: string): Promise { + const state: DocState = { + id, + manifest, + owner, + members: [{ did: owner, handle: ownerHandle, role: manifest.owner }], + log: [], + }; + this.#docs.set(id, state); + await this.#writeMeta(state); + return state; + } + + memberOf(id: string, did: string): Member | undefined { + return this.#docs.get(id)?.members.find((m) => m.did === did); + } + + async addMember(id: string, member: Member): Promise { + const doc = this.#docs.get(id); + if (!doc) return; + const existing = doc.members.find((m) => m.did === member.did); + if (existing) existing.role = member.role; + else doc.members.push(member); + await this.#writeMeta(doc); + } + + async removeMember(id: string, did: string): Promise { + const doc = this.#docs.get(id); + if (!doc) return; + doc.members = doc.members.filter((m) => m.did !== did); + await this.#writeMeta(doc); + } + + /** Append to the durable log and return the assigned entry. */ + async append(id: string, author: string, role: string, b64: string): Promise { + const doc = this.#docs.get(id)!; + const entry: LogEntry = { seq: doc.log.length + 1, author, role, b64 }; + doc.log.push(entry); + await Deno.writeTextFile(join(this.#root, 'docs', id, 'log.jsonl'), JSON.stringify(entry) + '\n', { + append: true, + }); + return entry; + } + + logSince(id: string, sinceSeq: number): LogEntry[] { + const doc = this.#docs.get(id); + if (!doc) return []; + return sinceSeq > 0 ? doc.log.filter((e) => e.seq > sinceSeq) : doc.log.slice(); + } + + async setSnapshot(id: string, atSeq: number, b64: string): Promise { + const doc = this.#docs.get(id); + if (!doc) return; + doc.snapshot = { atSeq, b64 }; + await Deno.writeTextFile(join(this.#root, 'docs', id, 'snapshot.json'), JSON.stringify(doc.snapshot)); + } + + snapshot(id: string): { atSeq: number; b64: string } | undefined { + return this.#docs.get(id)?.snapshot; + } + + async putInvite(inv: Invitation): Promise { + this.#invites.set(inv.token, inv); + await Deno.writeTextFile(join(this.#root, 'invites', `${inv.token}.json`), JSON.stringify(inv)); + const box = this.#inbox.get(inv.inviteeDid) ?? []; + box.push(inv); + this.#inbox.set(inv.inviteeDid, box); + await Deno.writeTextFile( + join(this.#root, 'inbox', `${encodeURIComponent(inv.inviteeDid)}.jsonl`), + JSON.stringify(inv) + '\n', + { append: true }, + ); + } + + invite(token: string): Invitation | undefined { + return this.#invites.get(token); + } + + /** KeyPackage directory: latest published package per DID. Stands in for + the per-user PDS record until AT sign-in lands; reuse is tolerated by + the MLS layer, so last-write-wins is correct enough here. */ + async putKeyPackage(did: string, b64: string): Promise { + this.#keyPackages.set(did, b64); + await Deno.writeTextFile(join(this.#root, 'keypackages', `${encodeURIComponent(did)}.b64`), b64); + } + + keyPackage(did: string): string | undefined { + return this.#keyPackages.get(did); + } + + inboxFor(did: string): Invitation[] { + return (this.#inbox.get(did) ?? []).slice(); + } + + async #readDoc(id: string): Promise { + const dir = join(this.#root, 'docs', id); + const meta = JSON.parse(await Deno.readTextFile(join(dir, 'meta.json'))) as { + manifest: SyncManifest; + owner: string; + members: Member[]; + }; + const log = (await this.#readLines(join(dir, 'log.jsonl'))) as LogEntry[]; + let snapshot: DocState['snapshot']; + try { + snapshot = JSON.parse(await Deno.readTextFile(join(dir, 'snapshot.json'))); + } catch { + snapshot = undefined; + } + return { id, manifest: meta.manifest, owner: meta.owner, members: meta.members, log, snapshot }; + } + + async #readLines(path: string): Promise { + let text: string; + try { + text = await Deno.readTextFile(path); + } catch { + return []; + } + return text + .split('\n') + .filter((l) => l.trim()) + .map((l) => JSON.parse(l) as T); + } + + async #writeMeta(doc: DocState): Promise { + const dir = join(this.#root, 'docs', doc.id); + await Deno.mkdir(dir, { recursive: true }); + await Deno.writeTextFile( + join(dir, 'meta.json'), + JSON.stringify({ manifest: doc.manifest, owner: doc.owner, members: doc.members }, null, 2), + ); + } +} diff --git a/relay/test.ts b/relay/test.ts new file mode 100644 index 0000000..464b3df --- /dev/null +++ b/relay/test.ts @@ -0,0 +1,211 @@ +// End-to-end exercise of the relay substrate: create, join, append+echo, +// ordering, presence with per-role visibility, write-permission enforcement, +// invite → inbox → accept, snapshot, and snapshot-based late join. +// Run: deno run -A relay/test.ts + +import { startRelay } from './main.ts'; +import { DevVerifier } from './auth.ts'; +import { fromB64, type ServerFrame, toB64 } from '../src/sync/protocol.ts'; + +let failures = 0; +const check = (label: string, ok: boolean, detail = '') => { + console.log(`${ok ? 'PASS' : 'FAIL'} ${label}${detail ? ` — ${detail}` : ''}`); + if (!ok) failures++; +}; +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); +const enc = (s: string) => new TextEncoder().encode(s); +const dec = (b: Uint8Array) => new TextDecoder().decode(b); + +class Client { + ws!: WebSocket; + frames: ServerFrame[] = []; + constructor(public did: string) {} + async connect(port: number, token: string, handle: string) { + this.ws = new WebSocket(`ws://127.0.0.1:${port}/ws?token=${token}&handle=${handle}`); + this.ws.addEventListener('message', (e) => this.frames.push(JSON.parse(e.data))); + await new Promise((res, rej) => { + this.ws.addEventListener('open', () => res()); + this.ws.addEventListener('error', () => rej(new Error('ws error'))); + }); + } + sendFrame(f: unknown) { + this.ws.send(JSON.stringify(f)); + } + msgs(docId: string) { + return this.frames.filter((f): f is Extract => f.t === 'msg' && f.docId === docId); + } + ephs() { + return this.frames.filter((f): f is Extract => f.t === 'eph'); + } + find(t: T) { + return this.frames.find((f) => f.t === t) as Extract | undefined; + } + last(t: T) { + return [...this.frames].reverse().find((f) => f.t === t) as Extract | undefined; + } + clear() { + this.frames = []; + } +} + +const dataDir = await Deno.makeTempDir({ prefix: 'ziran-relay-' }); +const verifier = new DevVerifier('test-secret'); +const relay = await startRelay({ port: 0, dataDir, verifier }); +const P = relay.port; + +const ALICE = 'did:plc:alice'; +const BOB = 'did:plc:bob'; +const CAROL = 'did:plc:carol'; + +const manifest = { + owner: 'author', + roles: { + author: { write: true, admin: true, presence: true }, + editor: { write: true, presence: true }, + watcher: { write: false, presence: true, seesRoles: ['author', 'editor'] }, + opponent: { write: true, max: 1 }, + }, +}; + +try { + const alice = new Client(ALICE); + await alice.connect(P, await verifier.mint(ALICE), 'alice.test'); + + // 1. Create a doc by joining with a manifest → owner role. + alice.sendFrame({ t: 'join', docId: 'doc1', manifest }); + await sleep(100); + const joined = alice.find('joined'); + check('owner joins and gets owner role', joined?.me.role === 'author', joined?.me.role); + + // 2. Append echoes back with a seq. + alice.sendFrame({ t: 'append', docId: 'doc1', id: 'a1', b64: toB64(enc('hello')) }); + await sleep(80); + check('append acked with seq 1', alice.find('ack')?.seq === 1); + check('append echoes to sender', alice.msgs('doc1').length === 1 && dec(fromB64(alice.msgs('doc1')[0].b64)) === 'hello'); + + // 3. Invite Bob as editor; it lands in his inbox (offline delivery). + alice.sendFrame({ t: 'invite', docId: 'doc1', inviteeDid: BOB, role: 'editor', id: 'i1', title: 'Doc One' }); + await sleep(80); + const invited = alice.last('invited'); + check('invite created', invited?.invitation.role === 'editor', invited?.invitation.role); + const token = invited!.invitation.token; + + // 4. Bob connects later, subscribes to his inbox, sees the pending invite. + const bob = new Client(BOB); + await bob.connect(P, await verifier.mint(BOB), 'bob.test'); + bob.sendFrame({ t: 'sub-inbox' }); + await sleep(80); + check('offline invite delivered on inbox subscribe', bob.find('inbox')?.invitation.token === token); + + // 5. Bob accepts → becomes a member, gets the snapshot pointer (none yet). + bob.sendFrame({ t: 'accept', token, id: 'ac1' }); + await sleep(80); + const accepted = bob.find('accepted'); + check('accept succeeds with role', accepted?.result.role === 'editor', accepted?.result.role); + + // 6. Bob joins and replays history (Alice's earlier "hello"). + bob.sendFrame({ t: 'join', docId: 'doc1' }); + await sleep(80); + check('late joiner replays the log', bob.msgs('doc1').some((m) => dec(fromB64(m.b64)) === 'hello')); + + // 7. Ordering: Bob appends, both see a consistent total order. + alice.clear(); + bob.clear(); + bob.sendFrame({ t: 'append', docId: 'doc1', id: 'b1', b64: toB64(enc('from-bob')) }); + await sleep(80); + check('editor may write', bob.find('ack')?.seq === 2); + check('both peers receive the new message', alice.msgs('doc1').length === 1 && bob.msgs('doc1').length === 1); + check('server-assigned order agrees', alice.msgs('doc1')[0].seq === bob.msgs('doc1')[0].seq); + + // 8. Write-permission: Carol as watcher cannot write. + alice.sendFrame({ t: 'invite', docId: 'doc1', inviteeDid: CAROL, role: 'watcher', id: 'i2' }); + await sleep(60); + const carol = new Client(CAROL); + await carol.connect(P, await verifier.mint(CAROL), 'carol.test'); + const cTok = alice.last('invited')!.invitation.token; // most recent + carol.sendFrame({ t: 'accept', token: cTok, id: 'ac2' }); + await sleep(60); + carol.sendFrame({ t: 'join', docId: 'doc1' }); + await sleep(60); + carol.clear(); + carol.sendFrame({ t: 'append', docId: 'doc1', id: 'c1', b64: toB64(enc('nope')) }); + await sleep(80); + check('watcher write is refused', carol.find('err')?.code === 'forbidden'); + + // 9. Per-role presence visibility: watcher sees author's presence… + carol.clear(); + alice.sendFrame({ t: 'ephemeral', docId: 'doc1', b64: toB64(enc('cursor:alice')) }); + await sleep(80); + check('watcher sees author presence', carol.ephs().some((e) => dec(fromB64(e.b64)) === 'cursor:alice')); + // …but the author does NOT see the watcher (author has no seesRoles → sees all, + // so instead verify a watcher does not see another watcher). + const dave = new Client('did:plc:dave'); + alice.sendFrame({ t: 'invite', docId: 'doc1', inviteeDid: 'did:plc:dave', role: 'watcher', id: 'i3' }); + await sleep(60); + await dave.connect(P, await verifier.mint('did:plc:dave'), 'dave.test'); + dave.sendFrame({ t: 'accept', token: alice.last('invited')!.invitation.token, id: 'ac3' }); + await sleep(60); + dave.sendFrame({ t: 'join', docId: 'doc1' }); + await sleep(60); + dave.clear(); + carol.sendFrame({ t: 'ephemeral', docId: 'doc1', b64: toB64(enc('cursor:carol')) }); + await sleep(80); + check('watcher does NOT see another watcher (per-role presence)', !dave.ephs().some((e) => dec(fromB64(e.b64)) === 'cursor:carol')); + + // 10. Snapshot + snapshot-based late join. + alice.sendFrame({ t: 'snapshot', docId: 'doc1', atSeq: 2, b64: toB64(enc('SNAPSHOT@2')) }); + await sleep(80); + const eve = new Client('did:plc:eve'); + alice.sendFrame({ t: 'invite', docId: 'doc1', inviteeDid: 'did:plc:eve', role: 'editor', id: 'i4' }); + await sleep(60); + await eve.connect(P, await verifier.mint('did:plc:eve'), 'eve.test'); + eve.sendFrame({ t: 'accept', token: alice.last('invited')!.invitation.token, id: 'ac4' }); + await sleep(80); + const eveAccept = eve.find('accepted'); + check('accept returns the snapshot', eveAccept?.result.snapshotB64 != null && + dec(fromB64(eveAccept.result.snapshotB64)) === 'SNAPSHOT@2' && eveAccept.result.atSeq === 2); + eve.sendFrame({ t: 'join', docId: 'doc1', sinceSeq: 2 }); + await sleep(80); + check('snapshot late-joiner replays only the tail', eve.msgs('doc1').every((m) => m.seq > 2)); + + // 11. Role cap: opponent max 1. + alice.sendFrame({ t: 'invite', docId: 'doc1', inviteeDid: 'did:plc:p1', role: 'opponent', id: 'o1' }); + await sleep(60); + const p1tok = alice.last('invited')!.invitation.token; + const p1 = new Client('did:plc:p1'); + await p1.connect(P, await verifier.mint('did:plc:p1'), 'p1'); + p1.sendFrame({ t: 'accept', token: p1tok, id: 'pa1' }); + await sleep(60); + alice.sendFrame({ t: 'invite', docId: 'doc1', inviteeDid: 'did:plc:p2', role: 'opponent', id: 'o2' }); + await sleep(80); + check('role cap enforced on second opponent', alice.frames.some((f) => f.t === 'err' && f.id === 'o2' && f.code === 'cap')); + + // 12. Auth: a bad token is refused at the door. + const badWs = new WebSocket(`ws://127.0.0.1:${P}/ws?token=garbage`); + const badResult = await new Promise((res) => { + badWs.addEventListener('open', () => res('opened')); + badWs.addEventListener('error', () => res('refused')); + badWs.addEventListener('close', () => res('refused')); + }); + check('invalid token refused at the door', badResult === 'refused'); + + // 13. Durability: reload the store from disk and confirm the doc survived. + relay.shutdown(); + await sleep(100); + const relay2 = await startRelay({ port: 0, dataDir, verifier }); + const late = new Client(ALICE); + await late.connect(relay2.port, await verifier.mint(ALICE), 'alice.test'); + late.sendFrame({ t: 'join', docId: 'doc1' }); + await sleep(100); + check('doc + log survive a relay restart', late.msgs('doc1').some((m) => dec(fromB64(m.b64)) === 'from-bob')); + relay2.shutdown(); +} finally { + await sleep(100); + await Deno.remove(dataDir, { recursive: true }).catch(() => {}); +} + +if (failures) { + console.error(`\n${failures} failure(s)`); + Deno.exit(1); +} +console.log('\nrelay substrate: all good'); diff --git a/src/components/zn-base-window.ts b/src/components/zn-base-window.ts index 8abc35c..448d8e8 100644 --- a/src/components/zn-base-window.ts +++ b/src/components/zn-base-window.ts @@ -145,6 +145,163 @@ export class ZnBaseWindow extends LitElement { outline: 2px solid var(--signal-strong); outline-offset: 2px; } + .id-signin { + margin-left: 0; + } + .id-chip { + display: inline-flex; + align-items: center; + gap: 7px; + font: inherit; + font-size: var(--text-sm); + font-weight: var(--w-medium); + color: var(--on-ink); + background: oklch(0.3 0.01 352); + border: 1px solid transparent; + border-radius: var(--r-full); + padding: 5px 14px; + cursor: pointer; + white-space: nowrap; + transition: background var(--t-fast) var(--ease-out); + } + .id-chip:hover { + background: oklch(0.35 0.012 352); + } + .id-chip:focus-visible { + outline: 2px solid var(--signal-strong); + outline-offset: 2px; + } + .id-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--signal); + } + .id-pop { + top: 60px; + right: var(--sp-5); + left: auto; + max-width: 320px; + } + .id-did { + margin: var(--sp-1) var(--sp-2); + font-family: var(--font-mono); + font-size: 0.72rem; + color: var(--ink-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .signin-form { + display: grid; + gap: var(--sp-2); + padding: var(--sp-2); + min-width: 260px; + } + .signin-form label { + font-size: var(--text-sm); + font-weight: var(--w-semibold); + color: var(--ink); + } + .signin-form input { + font: inherit; + font-size: var(--text-sm); + padding: 6px 12px; + border: 1px solid var(--line); + border-radius: var(--r-sm); + color: var(--ink); + background: var(--surface); + } + .signin-form input:focus-visible { + outline: 2px solid var(--signal); + outline-offset: 1px; + } + .menu button.signin-go { + font: inherit; + font-size: var(--text-sm); + font-weight: var(--w-medium); + border: 1px solid var(--ink); + background: var(--ink); + color: var(--surface); + border-radius: var(--r-full); + padding: 5px 14px; + cursor: pointer; + justify-self: start; + } + .menu button.signin-go:focus-visible { + outline: 2px solid var(--signal); + outline-offset: 2px; + } + .signin-fine { + margin: 0; + font-size: var(--text-xs); + color: var(--ink-muted); + line-height: 1.45; + text-wrap: pretty; + } + .invites { + display: grid; + gap: var(--sp-1); + padding: var(--sp-2) var(--sp-6); + border-bottom: 1px solid var(--line-soft); + background: var(--surface); + } + .invite-row { + display: flex; + align-items: center; + gap: var(--sp-2); + min-height: 32px; + min-width: 0; + } + .invite-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--signal); + flex: none; + } + .invite-text { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: var(--text-sm); + color: var(--ink); + } + .invite-accept, + .invite-dismiss { + font: inherit; + font-size: var(--text-sm); + font-weight: var(--w-medium); + border-radius: var(--r-full); + padding: 3px 12px; + cursor: pointer; + white-space: nowrap; + transition: border-color var(--t-fast) var(--ease-out), background var(--t-fast) var(--ease-out); + } + .invite-accept { + border: 1px solid var(--ink); + background: var(--ink); + color: var(--surface); + } + .invite-accept:hover { + background: var(--ink-strong, var(--ink)); + border-color: var(--ink-strong, var(--ink)); + } + .invite-dismiss { + border: 1px solid var(--line); + background: var(--surface); + color: var(--ink-muted); + } + .invite-dismiss:hover { + border-color: var(--ink-muted); + } + .invite-accept:focus-visible, + .invite-dismiss:focus-visible { + outline: 2px solid var(--signal); + outline-offset: 2px; + } .regions { flex: 1; display: grid; @@ -439,7 +596,9 @@ export class ZnBaseWindow extends LitElement { ⌘K + ${this.renderIdentity()} + ${this.renderInvitations()}

Recent

@@ -488,6 +647,76 @@ export class ZnBaseWindow extends LitElement { `; } + /** The identity affordance: your AT handle when signed in, a quiet way in + when not. Collaboration works either way — signing in makes you + reachable by handle across machines. */ + private renderIdentity() { + const at = desk.sync?.at; + if (at?.signedIn) { + return html` + + + `; + } + return html` + + + `; + } + + private idAction(fn: () => void) { + this.renderRoot.querySelector('.id-pop')?.hidePopover(); + fn(); + } + + /** Pending invitations: a strip between banner and regions, present only + when someone is actually waiting on you. */ + private renderInvitations() { + const invitations = desk.sync?.invitations ?? []; + if (!invitations.length) return nothing; + return html` +
+ ${invitations.map((inv) => html` +
+ + + ${inv.inviterHandle ?? inv.inviterDid} invited you to + “${inv.title}” as ${inv.role} + + + +
+ `)} +
+ `; + } + private tileIcon(url: string | undefined) { return url ? html`` diff --git a/src/components/zn-connections.ts b/src/components/zn-connections.ts index 979a62a..34a295b 100644 --- a/src/components/zn-connections.ts +++ b/src/components/zn-connections.ts @@ -72,6 +72,19 @@ export class ZnConnections extends LitElement { color: var(--ink-muted); transform: scale(0.9); } + .chip-live { + display: inline-grid; + place-items: center; + min-width: 16px; + height: 16px; + padding: 0 4px; + border-radius: var(--r-full); + background: var(--signal); + color: #fff; + font-size: 0.6875rem; + font-weight: var(--w-semibold); + line-height: 1; + } .mark { display: inline-grid; transform: scale(0.8); @@ -171,14 +184,75 @@ export class ZnConnections extends LitElement { outline: 2px solid var(--signal); outline-offset: 2px; } + ul.members { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 2px; + } + ul.members li { + display: flex; + align-items: center; + gap: var(--sp-2); + min-height: 24px; + min-width: 0; + } + .member-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .presence { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--signal); + flex: none; + } + form.invite { + display: flex; + gap: var(--sp-2); + min-width: 0; + } + form.invite input { + flex: 1; + min-width: 0; + font: inherit; + font-size: var(--text-sm); + padding: 3px 10px; + border: 1px solid var(--line); + border-radius: var(--r-full); + background: var(--surface); + color: var(--ink); + } + form.invite input::placeholder { + color: var(--ink-muted); + } + form.invite input:focus-visible, + form.invite select:focus-visible { + outline: 2px solid var(--signal); + outline-offset: 1px; + } + form.invite select { + font: inherit; + font-size: var(--text-sm); + border: 1px solid var(--line); + border-radius: var(--r-full); + background: var(--surface); + color: var(--ink); + padding: 3px 6px; + } `, ]; override render() { const d = this.doc; const iconUrl = docIconUrl(d); + const live = desk.syncDoc(d.id); const label = `Tile details for ${d.name}: ${d.damaged ? 'failed verification' : 'verified'}` + - (d.modelName ? `, made from the ${d.modelName} model` : ''); + (d.modelName ? `, made from the ${d.modelName} model` : '') + + (live ? `, live with ${live.members.length} ${live.members.length === 1 ? 'member' : 'members'}` : ''); return html` `; } + private renderSyncSection() { + const sync = desk.sync; + if (!sync?.relayOk) { + return html`

+ The relay is unreachable, so live sessions are paused. This document keeps working on + this machine. +

`; + } + const s = desk.syncDoc(this.doc.id); + if (!s) { + return html` +
+ Only on this machine + +
+

+ Going live keeps the document here and end-to-end encrypted — the relay only ever + carries ciphertext. You choose who joins, and as what. +

+ `; + } + const roles = ['editor', 'watcher']; + return html` +
    + ${s.members.map((m) => html` +
  • + + ${m.handle ?? m.did} + ${m.did === sync.identity.did ? `${m.role} · you` : m.role} +
  • + `)} +
+
+ + + +
+

+ They get the invitation inside Ziran and as a link you can pass along. Accepting + downloads the document to their machine first, then joins them live. +

+ `; + } + + private async submitInvite(e: Event) { + e.preventDefault(); + const form = e.target as HTMLFormElement; + const did = (form.elements.namedItem('did') as HTMLInputElement).value.trim(); + const role = (form.elements.namedItem('role') as HTMLSelectElement).value; + if (!did) return; + const link = await desk.invite(this.doc.id, did, role); + if (link) (form.elements.namedItem('did') as HTMLInputElement).value = ''; + } + private renderModelSection() { const d = this.doc; const saved = d.modelId ? desk.models.find((m) => m.id === d.modelId) : undefined; diff --git a/src/components/zn-doc-window.ts b/src/components/zn-doc-window.ts index 72ff9ed..c9625cf 100644 --- a/src/components/zn-doc-window.ts +++ b/src/components/zn-doc-window.ts @@ -77,6 +77,30 @@ export class ZnDocWindow extends LitElement { place-items: center; color: var(--ink-muted); } + .bar .live { + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: var(--sp-2); + font-size: var(--text-sm); + color: var(--ink-muted); + white-space: nowrap; + } + .bar .live-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--signal); + } + @media (prefers-reduced-motion: no-preference) { + .bar .live-dot { + animation: live-pulse 2.4s var(--ease-out) infinite; + } + @keyframes live-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.45; } + } + } .bar .title { font-weight: var(--w-semibold); font-size: var(--text-ui); @@ -331,6 +355,19 @@ export class ZnDocWindow extends LitElement { return rec.panes.find((p) => p.id === this.focusPaneId) ?? rec.panes[0]!; } + /** The banner space reserved for sync info: present only when the document + is in a live session. The dot is signal pink — semantic (live), not + decoration. */ + private renderLive(docId: string) { + const s = desk.syncDoc(docId); + if (!s) return nothing; + const n = s.members.length; + return html` + + ${n === 1 ? 'live · only you' : `live · ${n} people`} + `; + } + private tileIcon(docId: string, size = 18) { const doc = desk.getDoc(docId); const url = doc ? docIconUrl(doc) : undefined; @@ -368,6 +405,7 @@ export class ZnDocWindow extends LitElement { @dblclick=${() => this.beginRename()} >${activeDoc.name}`} + ${this.renderLive(activeDoc.id)} ` : nothing} diff --git a/src/server/api.ts b/src/server/api.ts index 85485b3..621f565 100644 --- a/src/server/api.ts +++ b/src/server/api.ts @@ -6,6 +6,8 @@ import mime from 'mime'; import { parseTile, readResource, readTileData, renameTile, writeTileData } from './tilefile.ts'; import { pickOpen, pickSave, revealInFolder } from './dialogs.ts'; import type { ZiranStore } from './store.ts'; +import type { SyncManager } from './sync.ts'; +import type { AtSession } from './atsession.ts'; const staticRoots = [ join(Deno.cwd(), 'static'), @@ -55,11 +57,31 @@ function safeEqual(a: string, b: string): boolean { return diff === 0; } -export function createHandler(store: ZiranStore): (req: Request) => Promise { +export function createHandler(store: ZiranStore, sync?: SyncManager, at?: AtSession): (req: Request) => Promise { return async (req: Request): Promise => { const url = new URL(req.url); const { pathname } = url; + // OAuth redirect landing — deliberately outside the /api key gate (the + // browser arrives bare); integrity comes from the OAuth state parameter, + // which the client library verifies. + if (pathname === '/oauth/callback' && at) { + try { + const { handle, did } = await at.callback(url.searchParams); + return new Response( + `Signed in — Ziran + +
Z
+

You’re signed in as ${handle ?? did}

+

Head back to Ziran — this tab is done.

+ `, + { headers: { 'content-type': 'text/html; charset=utf-8' } }, + ); + } catch (e) { + return new Response(`sign-in failed: ${(e as Error).message}`, { status: 400 }); + } + } + // The API is for the shell UI only, two gates deep: // 1. Bearer token, minted at launch and known only to pages the server // itself navigated to (any local process can reach 127.0.0.1 — the @@ -229,6 +251,9 @@ export function createHandler(store: ZiranStore): (req: Request) => Promise Promise { + #file: string; + #data: Record | undefined; + + constructor(name: string) { + this.#file = join(syncDir(), name); + } + + async #load(): Promise> { + if (!this.#data) { + try { + this.#data = JSON.parse(await Deno.readTextFile(this.#file)); + } catch { + this.#data = {}; + } + } + return this.#data!; + } + + async get(key: string): Promise { + return (await this.#load())[key]; + } + async set(key: string, value: V): Promise { + const data = await this.#load(); + data[key] = value; + await Deno.writeTextFile(this.#file, JSON.stringify(data)); + } + async del(key: string): Promise { + const data = await this.#load(); + delete data[key]; + await Deno.writeTextFile(this.#file, JSON.stringify(data)); + } +} + +function openInBrowser(url: string): void { + const cmd = Deno.build.os === 'darwin' + ? ['open', url] + : Deno.build.os === 'windows' + ? ['cmd', '/c', 'start', '', url] + : ['xdg-open', url]; + try { + new Deno.Command(cmd[0]!, { args: cmd.slice(1) }).spawn().unref(); + } catch { + // headless / no opener: the URL is surfaced to the UI regardless + } +} + +export class AtSession { + did: string | null = null; + handle: string | null = null; + + #client?: NodeOAuthClient; + #session?: OAuthSession; + #agent?: Agent; + #currentFile = ''; + /** Called when the signed-in identity changes (sign-in or sign-out). */ + onChange: (() => Promise | void) | undefined; + + async init(appPort: number): Promise { + this.#currentFile = join(syncDir(), 'at-current.json'); + const redirectUri = `http://127.0.0.1:${appPort}/oauth/callback`; + // Loopback development client: no hosted metadata document needed; the + // authorization server derives the metadata from the client_id itself. + const clientId = `http://localhost?redirect_uri=${encodeURIComponent(redirectUri)}&scope=${ + encodeURIComponent(SCOPE) + }`; + this.#client = new NodeOAuthClient({ + // Deno: the default AtprotoHandleResolverNode eagerly builds a + // Node-undici SSRF wrapper that doesn't exist here. Native fetch + + // the public HTTP resolver sidestep the whole node-only path. + fetch: globalThis.fetch, + handleResolver: 'https://public.api.bsky.app', + clientMetadata: { + client_id: clientId, + client_name: 'Ziran', + redirect_uris: [redirectUri], + scope: SCOPE, + grant_types: ['authorization_code', 'refresh_token'], + response_types: ['code'], + application_type: 'native', + token_endpoint_auth_method: 'none', + dpop_bound_access_tokens: true, + }, + stateStore: new FileStore('at-oauth-state.json'), + sessionStore: new FileStore('at-oauth-sessions.json'), + }); + + // Restore the previous sign-in, if any. + try { + const { did } = JSON.parse(await Deno.readTextFile(this.#currentFile)); + if (typeof did === 'string') await this.#activate(did); + } catch { + // signed out + } + } + + get signedIn(): boolean { + return this.did !== null; + } + + async #activate(did: string): Promise { + this.#session = await this.#client!.restore(did); + this.#agent = new Agent(this.#session); + this.did = did; + try { + this.handle = (await resolveIdentity(did)).handle; + } catch { + this.handle = null; + } + } + + /** Begin sign-in: returns the authorization URL (also opened in the + user's browser). */ + async signIn(identifier: string): Promise { + if (!this.#client) throw new Error('sign-in is not ready yet'); + const url = await this.#client.authorize(identifier.trim().replace(/^@/, ''), { scope: SCOPE }); + openInBrowser(url.href); + return url.href; + } + + /** OAuth redirect landing: exchange the code, persist, activate. */ + async callback(params: URLSearchParams): Promise<{ did: string; handle: string | null }> { + const { session } = await this.#client!.callback(params); + await Deno.writeTextFile(this.#currentFile, JSON.stringify({ did: session.did })); + await this.#activate(session.did); + await this.onChange?.(); + return { did: this.did!, handle: this.handle }; + } + + async signOut(): Promise { + const did = this.did; + this.did = null; + this.handle = null; + this.#agent = undefined; + this.#session = undefined; + await Deno.remove(this.#currentFile).catch(() => {}); + if (did) await this.#client?.revoke(did).catch(() => {}); + await this.onChange?.(); + } + + /** Mint a relay token at the user's PDS (service-auth, ~60s, DID-signed). */ + async relayToken(relayDid: string): Promise { + if (!this.#agent) throw new Error('not signed in'); + const res = await this.#agent.com.atproto.server.getServiceAuth({ aud: relayDid, lxm: RELAY_LXM }); + return res.data.token; + } + + /** Publish this identity's MLS KeyPackage as a record in their own repo. */ + async publishKeyPackage(b64: string): Promise { + if (!this.#agent || !this.did) throw new Error('not signed in'); + await this.#agent.com.atproto.repo.putRecord({ + repo: this.did, + collection: KP_COLLECTION, + rkey: 'self', + record: { $type: KP_COLLECTION, keyPackage: b64, createdAt: new Date().toISOString() }, + }); + } +} + +/** Fetch a member's published KeyPackage from their PDS (unauthenticated — + repo records are public). Returns null if they never published one. */ +export async function fetchPdsKeyPackage(did: string): Promise { + try { + const identity = await resolveIdentity(did); + if (!identity.pds) return null; + const url = `${identity.pds.replace(/\/+$/, '')}/xrpc/com.atproto.repo.getRecord?repo=${ + encodeURIComponent(did) + }&collection=${KP_COLLECTION}&rkey=self`; + const res = await fetch(url); + if (!res.ok) { + await res.body?.cancel(); + return null; + } + const data = await res.json(); + const kp = data?.value?.keyPackage; + return typeof kp === 'string' ? kp : null; + } catch { + return null; + } +} diff --git a/src/server/paths.ts b/src/server/paths.ts index 90e659c..498d577 100644 --- a/src/server/paths.ts +++ b/src/server/paths.ts @@ -25,9 +25,13 @@ export const modelsDir = () => join(appDataDir(), 'models'); export const importsDir = () => join(appDataDir(), 'imports'); export const recentsFile = () => join(appDataDir(), 'recents.json'); export const sessionFile = () => join(appDataDir(), 'session.json'); +export const syncDir = () => join(appDataDir(), 'sync'); +export const groupsDir = () => join(syncDir(), 'groups'); +export const identityFile = () => join(syncDir(), 'identity.json'); +export const syncRegistryFile = () => join(syncDir(), 'docs.json'); export async function ensureDirs(): Promise { - for (const dir of [documentsDir(), modelsDir(), importsDir()]) { + for (const dir of [documentsDir(), modelsDir(), importsDir(), groupsDir()]) { await Deno.mkdir(dir, { recursive: true }); } } diff --git a/src/server/register.ts b/src/server/register.ts index 4aee20f..845598c 100644 --- a/src/server/register.ts +++ b/src/server/register.ts @@ -27,6 +27,10 @@ async function registerWindows(exe: string): Promise { await reg([`HKCU\\Software\\Classes\\${PROG_ID}`, '/ve', '/d', 'DASL Web Tile']); await reg([`HKCU\\Software\\Classes\\${PROG_ID}\\DefaultIcon`, '/ve', '/d', `"${exe}",0`]); await reg([`HKCU\\Software\\Classes\\${PROG_ID}\\shell\\open\\command`, '/ve', '/d', `"${exe}" "%1"`]); + // ziran:// deep links (invitation accepts). + await reg([`HKCU\\Software\\Classes\\ziran`, '/ve', '/d', 'URL:Ziran']); + await reg([`HKCU\\Software\\Classes\\ziran`, '/v', 'URL Protocol', '/d', '']); + await reg([`HKCU\\Software\\Classes\\ziran\\shell\\open\\command`, '/ve', '/d', `"${exe}" "%1"`]); } async function registerLinux(exe: string): Promise { @@ -59,7 +63,7 @@ Exec="${exe}" %f Icon=com.berjon.ziran Terminal=false Categories=Office; -MimeType=${MIME}; +MimeType=${MIME};x-scheme-handler/ziran; `, ); // Icon (shipped in the compiled app's VFS under build/). @@ -73,6 +77,7 @@ MimeType=${MIME}; await run('update-mime-database', [join(dataHome, 'mime')]); await run('update-desktop-database', [apps]); await run('xdg-mime', ['default', 'com.berjon.ziran.desktop', MIME]); + await run('xdg-mime', ['default', 'com.berjon.ziran.desktop', 'x-scheme-handler/ziran']); } /** Run once per profile; never throws. */ diff --git a/src/server/store.ts b/src/server/store.ts index 9b4bfec..c8dc49a 100644 --- a/src/server/store.ts +++ b/src/server/store.ts @@ -85,6 +85,10 @@ export class ZiranStore { openOsWindow: ((winId: string, url: string, title: string, bounds?: Bounds) => boolean) | undefined; /** Last reported geometry of the base window. */ mainBounds: Bounds | undefined; + /** Collaboration summary folded into every state snapshot (set by main.ts). */ + syncSummary: (() => unknown) | undefined; + /** Re-keys the sync registry when a move changes a path-derived id. */ + onDocMoved: ((oldId: string, newId: string, newPath: string) => void) | undefined; #sockets = new Set(); #winSockets = new Map>(); @@ -353,9 +357,25 @@ export class ZiranStore { this.broadcast(); } - /** First run: copy bundled starter tiles into the models dir. */ + /** Seed bundled starter tiles the user's library doesn't have yet (by + model id, so new starters reach existing profiles without touching + models the user changed or removed deliberately… removals do come + back, but only when the app ships a starter under that id). */ async seedStarterModels(): Promise { - for await (const _ of Deno.readDir(modelsDir())) return; // not empty: done + const have = new Set(); + try { + for await (const entry of Deno.readDir(modelsDir())) { + if (!entry.isFile || !entry.name.endsWith('.tile')) continue; + try { + const tile = await parseTile(join(modelsDir(), entry.name), { verify: false }); + if (tile.manifest.model?.id) have.add(tile.manifest.model.id); + } catch { + // unreadable model; leave it alone + } + } + } catch { + // models dir missing; ensureDirs created it during load + } const roots = [join(Deno.cwd(), 'tiles', 'built'), new URL('../../tiles/built/', import.meta.url).pathname]; for (const root of roots) { try { @@ -363,6 +383,9 @@ export class ZiranStore { if (!entry.isFile || !entry.name.endsWith('.tile')) continue; const src = join(root, entry.name); try { + const tile = await parseTile(src, { verify: false }); + const id = tile.manifest.model?.id; + if (!id || have.has(id)) continue; const model = await writeModelTile(src, join(modelsDir(), entry.name)); console.log(`seeded starter model ${model.name ?? entry.name}`); } catch (err) { @@ -485,6 +508,7 @@ export class ZiranStore { this.open.delete(id); this.recents = this.recents.filter((r) => r.path !== doc.info.path && r.path !== destPath); const info = await this.openPath(destPath); + this.onDocMoved?.(id, info.id, destPath); // Ids are path-derived: remap the moved doc inside every window record so // open windows carry on undisturbed. for (const win of this.windows) { @@ -539,9 +563,22 @@ export class ZiranStore { mainBounds: this.mainBounds, tileServerPort: this.tileServerPort, desktop: this.desktop, + sync: this.syncSummary?.(), }; } + /** Push a one-off event (sync traffic) to every window socket. */ + broadcastEvent(event: Record): void { + const msg = JSON.stringify(event); + for (const ws of this.#sockets) { + try { + ws.send(msg); + } catch { + this.#sockets.delete(ws); + } + } + } + /** Sockets identify which window they render ('base' or a win id). When a window's last socket closes and no reload reclaims it briefly, the OS window is gone: drop its record and release its docs. */ diff --git a/src/server/sync.ts b/src/server/sync.ts new file mode 100644 index 0000000..b46a9c1 --- /dev/null +++ b/src/server/sync.ts @@ -0,0 +1,465 @@ +// The runtime's collaboration manager: owns the identity, the E2EE transport, +// and every live document session. MLS runs here, host-side — the UI sees +// plaintext + membership events over the authenticated window sockets, tiles +// see only the tp-sync frames the UI forwards down. Tiles never touch keys, +// invitations, or membership (the runtime chrome is the only writer of those). +// +// Identity is a generated dev DID until AT sign-in lands (ZIRAN_DID / +// ZIRAN_HANDLE override; swapping in atproto OAuth + service-auth changes +// only this file's identity block and the relay's Verifier). + +import { join } from '@std/path'; +import type { Invitation, Member, Session, SyncManifest } from '../sync/protocol.ts'; +import { fromB64, toB64 } from '../sync/protocol.ts'; +import { RelaySyncTransport } from '../sync/relay-client.ts'; +import { type KeyPackageDirectory, MlsSyncTransport, RelayKeyPackageDirectory } from '../sync/mls-transport.ts'; +import { DevVerifier } from '../../relay/auth.ts'; +import { resolveIdentity } from '../sync/atproto.ts'; +import { type AtSession, fetchPdsKeyPackage } from './atsession.ts'; +import { documentsDir, groupsDir, identityFile, syncRegistryFile } from './paths.ts'; +import type { ZiranStore } from './store.ts'; + +/** Runtime-enforced defaults when a tile's manifest declares no sync roles. */ +const DEFAULT_MANIFEST: SyncManifest = { + owner: 'author', + roles: { + author: { write: true, admin: true }, + editor: { write: true }, + watcher: { write: false }, + }, +}; + +interface SharedDoc { + /** Path-derived local doc id (changes on move; path is re-derived). */ + localId: string; + /** Stable cross-member channel id. */ + syncId: string; + path: string; + role: string; + manifest: SyncManifest; + /** Seq the tile's persisted state is known to reflect (snapshot point). */ + ackedSeq: number; +} + +interface BufferedFrame { + seq: number; + author: string; + role: string; + b64: string; +} + +interface LiveDoc { + rec: SharedDoc; + session: Session; + /** Decrypted messages past ackedSeq, waiting for (re)attached tiles. */ + buffer: BufferedFrame[]; + lastForwardedSeq: number; + roster: Member[]; +} + +export interface SyncSummary { + identity: { did: string; handle: string }; + at: { signedIn: boolean; handle?: string; did?: string }; + relayOk: boolean; + invitations: Array>; + docs: Record; +} + +export class SyncManager { + did = ''; + handle = ''; + relayUrl = ''; + relayOk = false; + + #store: ZiranStore; + #at?: AtSession; + #devDid = ''; + #devHandle = ''; + #base?: RelaySyncTransport; + #transport?: MlsSyncTransport; + #registry: SharedDoc[] = []; + #live = new Map(); // by localId + #invitations = new Map(); // pending, token → invitation + #dismissed = new Set(); + + constructor(store: ZiranStore) { + this.#store = store; + } + + /* ——— boot ——— */ + + async init(relayUrl: string, at?: AtSession): Promise { + this.relayUrl = relayUrl; + this.#at = at; + if (at) at.onChange = () => this.#reconnect(); + await this.#loadDevIdentity(); + try { + this.#registry = JSON.parse(await Deno.readTextFile(syncRegistryFile())); + } catch { + this.#registry = []; + } + await this.#connect(); + } + + /** Signed in → the real AT identity; otherwise the local dev identity. */ + #applyIdentity(): void { + if (this.#at?.signedIn) { + this.did = this.#at.did!; + this.handle = this.#at.handle ?? this.#at.did!; + } else { + this.did = this.#devDid; + this.handle = this.#devHandle; + } + } + + async #connect(): Promise { + this.#applyIdentity(); + const relayDid = Deno.env.get('ZIRAN_RELAY_DID') ?? 'did:web:relay.ziran.space'; + const base = new RelaySyncTransport({ + url: this.relayUrl, + did: this.did, + handle: this.handle, + // Signed in: a service-auth JWT minted by the user's own PDS. Dev + // identity: an HMAC token under the relay's shared dev secret. + token: () => + this.#at?.signedIn + ? this.#at.relayToken(relayDid) + : new DevVerifier(Deno.env.get('ZIRAN_RELAY_SECRET') ?? 'dev-secret').mint(this.did), + manifestFor: (syncId) => this.#registry.find((r) => r.syncId === syncId)?.manifest, + }); + this.#base = base; + const relayDir = new RelayKeyPackageDirectory(base); + // KeyPackage discovery prefers the invitee's own PDS record (works + // across relays, survives relay resets); the relay directory is the + // fallback and the home of dev identities. + const directory: KeyPackageDirectory = { + publish: async (did, kp) => { + await relayDir.publish(did, kp); + if (this.#at?.signedIn) { + await this.#at.publishKeyPackage(toB64(kp)).catch((err) => + console.warn('sync: could not publish KeyPackage to PDS:', (err as Error).message) + ); + } + }, + fetch: async (did) => { + const pds = await fetchPdsKeyPackage(did); + if (pds) return fromB64(pds); + return relayDir.fetch(did); + }, + }; + this.#transport = new MlsSyncTransport({ + base, + did: this.did, + directory, + groupStore: { + load: async (syncId) => { + try { + return await Deno.readFile(join(groupsDir(), `${syncId}.bin`)); + } catch { + return null; + } + }, + save: async (syncId, state) => { + const tmp = join(groupsDir(), `.${syncId}.tmp`); + await Deno.writeFile(tmp, state); + await Deno.rename(tmp, join(groupsDir(), `${syncId}.bin`)); + }, + }, + }); + + try { + await this.#transport.register(); + this.relayOk = true; + this.#watchInbox(); + // Rejoin every registered shared doc so offline edits flow both ways + // even before its window opens. Docs shared under a different identity + // than the current one fail membership and are skipped with a warning. + for (const rec of this.#registry) { + await this.#rejoin(rec).catch((err) => + console.warn(`sync: could not rejoin ${rec.path}:`, (err as Error).message) + ); + } + } catch (err) { + this.relayOk = false; + console.warn('sync: relay unreachable, collaboration paused:', (err as Error).message); + } + this.#store.broadcast(); + } + + /** Tear down and rebuild the transport under the (new) current identity. */ + async #reconnect(): Promise { + this.#base?.close(); + this.#live.clear(); + this.#invitations.clear(); + this.relayOk = false; + await this.#connect(); + } + + async #loadDevIdentity(): Promise { + const envDid = Deno.env.get('ZIRAN_DID'); + const envHandle = Deno.env.get('ZIRAN_HANDLE'); + try { + const saved = JSON.parse(await Deno.readTextFile(identityFile())); + this.#devDid = envDid ?? saved.did; + this.#devHandle = envHandle ?? saved.handle; + } catch { + this.#devDid = envDid ?? + `did:ziran:${[...crypto.getRandomValues(new Uint8Array(6))].map((b) => b.toString(16).padStart(2, '0')).join('')}`; + this.#devHandle = envHandle ?? `${Deno.env.get('USER') ?? 'someone'}.ziran.local`; + } + await Deno.writeTextFile(identityFile(), JSON.stringify({ did: this.#devDid, handle: this.#devHandle })); + } + + async #persistRegistry(): Promise { + await Deno.writeTextFile(syncRegistryFile(), JSON.stringify(this.#registry, null, 2)); + } + + /* ——— inbox ——— */ + + #watchInbox(): void { + const transport = this.#transport!; + (async () => { + for await (const inv of transport.inbox()) { + if (this.#transport !== transport) break; // identity switched; stale loop + if (this.#dismissed.has(inv.token)) continue; + // Already a member (or our own doc)? Then this invite is stale. + if (this.#registry.some((r) => r.syncId === inv.docId)) continue; + this.#invitations.set(inv.token, inv); + this.#store.broadcast(); + } + })().catch(() => {}); + } + + /* ——— doc lookup ——— */ + + byLocal(localId: string): SharedDoc | undefined { + return this.#registry.find((r) => r.localId === localId); + } + + /** Doc ids are path-derived, so a file move re-keys the registry entry + (called via the store's move hook). */ + moved(oldId: string, newId: string, newPath: string): void { + const rec = this.#registry.find((r) => r.localId === oldId); + if (!rec) return; + const live = this.#live.get(oldId); + rec.localId = newId; + rec.path = newPath; + if (live) { + this.#live.delete(oldId); + this.#live.set(newId, live); + } + this.#persistRegistry().catch(() => {}); + } + + /* ——— sharing ——— */ + + /** Start sharing an open document: create the sync channel, join it, and + upload the initial snapshot (the tile file itself). */ + async share(localId: string): Promise { + this.#requireRelay(); + const doc = this.#store.doc(localId); + if (!doc) throw new Error('document is not open'); + const byPath = this.#registry.find((r) => r.path === doc.info.path); + if (byPath && byPath.localId !== localId) this.moved(byPath.localId, localId, doc.info.path); + const existing = this.byLocal(localId); + if (existing) return existing; + + const manifest = (doc.tile.manifest as { sync?: { roles?: SyncManifest['roles']; owner?: string } }).sync; + const rec: SharedDoc = { + localId, + syncId: crypto.randomUUID().replace(/-/g, ''), + path: doc.info.path, + role: '', + manifest: manifest?.roles + ? { owner: manifest.owner ?? Object.keys(manifest.roles)[0]!, roles: manifest.roles } + : DEFAULT_MANIFEST, + ackedSeq: 0, + }; + rec.role = rec.manifest.owner; + this.#registry.push(rec); + await this.#persistRegistry(); + await this.#join(rec, 0); + // Initial snapshot so the very first invitee downloads-then-syncs. + await this.uploadSnapshot(rec); + this.#store.broadcast(); + return rec; + } + + async #rejoin(rec: SharedDoc): Promise { + const restored = await this.#transport!.restoreGroup(rec.syncId); + if (!restored) throw new Error('no persisted group state'); + await this.#join(rec, rec.ackedSeq); + } + + async #join(rec: SharedDoc, sinceSeq: number): Promise { + const session = await this.#transport!.join(rec.syncId, { sinceSeq }); + const live: LiveDoc = { rec, session, buffer: [], lastForwardedSeq: sinceSeq, roster: session.roster() }; + this.#live.set(rec.localId, live); + session.onMessage((m) => { + live.lastForwardedSeq = Math.max(live.lastForwardedSeq, m.seq); + const frame = { seq: m.seq, author: m.author, role: m.role, b64: toB64(m.bytes) }; + live.buffer.push(frame); + this.#store.broadcastEvent({ type: 'sync-msg', docId: rec.localId, ...frame }); + }); + session.onEphemeral((m) => { + this.#store.broadcastEvent({ + type: 'sync-eph', + docId: rec.localId, + author: m.author, + role: m.role, + b64: toB64(m.bytes), + }); + }); + session.onRoster((roster) => { + live.roster = roster; + this.#store.broadcastEvent({ type: 'sync-roster', docId: rec.localId, roster }); + this.#store.broadcast(); // membership shows in panel + banner state + }); + // A tile already open when its doc goes live must hear 'ready' without + // re-announcing itself. + this.#store.broadcastEvent({ type: 'sync-ready', docId: rec.localId, me: session.me, roster: live.roster }); + } + + /* ——— invitations out ——— */ + + async invite(localId: string, identifier: string, role: string): Promise<{ invitation: Invitation; link: string }> { + this.#requireRelay(); + const rec = this.byLocal(localId) ?? (await this.share(localId)); + if (!rec.manifest.roles[role]) throw new Error(`this document has no “${role}” role`); + // Handles resolve to DIDs (with backlink verification); dev DIDs and + // real DIDs pass through as-is. + let inviteeDid = identifier.trim(); + if (!inviteeDid.startsWith('did:')) { + inviteeDid = (await resolveIdentity(inviteeDid)).did; + } + if (inviteeDid === this.did) throw new Error('that is your own identity'); + const doc = this.#store.doc(localId); + const invitation = await this.#transport!.invite(rec.syncId, inviteeDid, role, doc?.info.name); + // Snapshot AFTER the add-commit: the invitee joins at the post-add epoch + // and can only decrypt a snapshot encrypted at (or after) that epoch. + await this.uploadSnapshot(rec); + this.#store.broadcast(); + return { invitation, link: this.inviteLink(invitation.token) }; + } + + inviteLink(token: string): string { + // ws://host:port/ws → http://host:port/i/ + return this.relayUrl.replace(/^ws/, 'http').replace(/\/ws$/, '') + `/i/${token}`; + } + + /* ——— invitations in ——— */ + + async accept(token: string): Promise<{ path: string }> { + this.#requireRelay(); + const inv = this.#invitations.get(token) ?? (await this.#base!.getInvite(token)) ?? undefined; + if (!inv) throw new Error('invitation not found — it may have been withdrawn'); + // Download-then-sync: the snapshot IS the tile file; verify + open happens + // through the normal engine path once we write it into documents/. + const result = await this.#transport!.accept(token); + if (!result.snapshot) throw new Error('the document snapshot is missing — ask for a fresh invitation'); + const stem = (inv.title || 'shared').replace(/[^\w.-]+/g, '-').replace(/^-+|-+$/g, '') || 'shared'; + let path = join(documentsDir(), `${stem}.tile`); + try { + await Deno.stat(path); + path = join(documentsDir(), `${stem}-${token.slice(0, 6)}.tile`); + } catch { + // free name + } + await Deno.writeFile(path, result.snapshot); + const rec: SharedDoc = { + localId: '', // set after openPath derives it + syncId: result.docId, + path, + role: result.role, + manifest: DEFAULT_MANIFEST, // relay enforces; local copy is advisory + ackedSeq: result.atSeq, + }; + this.#invitations.delete(token); + this.#dismissed.add(token); + // openPath verifies CIDs and registers recents; its id keys our registry. + const info = await this.#store.openPath(path); + rec.localId = info.id; + this.#registry.push(rec); + await this.#persistRegistry(); + await this.#join(rec, result.atSeq); + this.#store.broadcast(); + return { path }; + } + + dismiss(token: string): void { + this.#invitations.delete(token); + this.#dismissed.add(token); + this.#store.broadcast(); + } + + /* ——— tile traffic (called from the API on behalf of the UI bridge) ——— */ + + /** Tile (re)announced itself: flush everything past its persisted state. */ + attach(localId: string): { me: Member; roster: Member[]; buffered: BufferedFrame[] } | null { + const live = this.#live.get(localId); + if (!live) return null; + return { + me: live.session.me, + roster: live.roster, + buffered: live.buffer.filter((m) => m.seq > live.rec.ackedSeq), + }; + } + + async send(localId: string, b64: string): Promise { + const live = this.#live.get(localId); + if (!live) throw new Error('document is not in a live session'); + return await live.session.append(fromB64(b64)); + } + + ephemeral(localId: string, b64: string): void { + this.#live.get(localId)?.session.ephemeral(fromB64(b64)); + } + + /** The tile persisted its state (putData): everything forwarded so far is + now reflected in the tile file. Advance the snapshot point, trim the + buffer, and upload the fresh snapshot. */ + async onLocalDataWrite(localId: string): Promise { + const live = this.#live.get(localId); + if (!live) return; + live.rec.ackedSeq = live.lastForwardedSeq; + live.buffer = live.buffer.filter((m) => m.seq > live.rec.ackedSeq); + await this.#persistRegistry(); + await this.uploadSnapshot(live.rec).catch(() => {}); + } + + async uploadSnapshot(rec: SharedDoc): Promise { + const live = this.#live.get(rec.localId); + if (!live) return; + const bytes = await Deno.readFile(rec.path); + await live.session.snapshot(bytes, rec.ackedSeq); + } + + /* ——— state for the UI ——— */ + + summary(): SyncSummary { + const docs: SyncSummary['docs'] = {}; + for (const [localId, live] of this.#live) { + docs[localId] = { + syncId: live.rec.syncId, + role: live.rec.role, + members: live.roster, + inviteBase: this.relayUrl.replace(/^ws/, 'http').replace(/\/ws$/, ''), + }; + } + return { + identity: { did: this.did, handle: this.handle }, + at: this.#at?.signedIn + ? { signedIn: true, handle: this.#at.handle ?? undefined, did: this.#at.did ?? undefined } + : { signedIn: false }, + relayOk: this.relayOk, + invitations: [...this.#invitations.values()].map(({ payload: _p, ...rest }) => rest), + docs, + }; + } + + #requireRelay(): void { + if (!this.relayOk || !this.#transport) { + throw new Error('the relay is unreachable — collaboration is paused'); + } + } +} diff --git a/src/server/tileserver.ts b/src/server/tileserver.ts index e6f8b23..f908f39 100644 --- a/src/server/tileserver.ts +++ b/src/server/tileserver.ts @@ -43,7 +43,7 @@ const TILE_HEADERS: Record = { 'x-robots-tag': 'noai, noimageai', }; -const VENDORED = new Set(['shuttle.js', 'data.js', 'store.js']); +const VENDORED = new Set(['shuttle.js', 'data.js', 'store.js', 'sync.js']); const vendorRoots = [ toFileUrl(join(Deno.cwd(), 'vendor', 'web-tiles')) + '/', new URL('../../vendor/web-tiles/', import.meta.url), diff --git a/src/state/backend.ts b/src/state/backend.ts index d46857d..305b0b9 100644 --- a/src/state/backend.ts +++ b/src/state/backend.ts @@ -2,7 +2,7 @@ // WebSocket with auto-reconnect. All server truth arrives as full state // snapshots; the desk store consumes them. -import type { Bounds, DocInfo, DocWindowRec, ModelEntry, ServerState } from '../types.ts'; +import type { Bounds, DocInfo, DocWindowRec, ModelEntry, ServerState, SyncFrame, SyncMember } from '../types.ts'; export interface OpenResult { doc: DocInfo; @@ -33,6 +33,8 @@ async function post(url: string, body?: unknown): Promise { export const backend = { onState: undefined as ((state: ServerState) => void) | undefined, + /** Non-state events (sync traffic); fan-out to whoever subscribes. */ + events: new EventTarget(), connect(winId: string): void { const ws = new WebSocket(keyed(`ws://${location.host}/api/events?win=${encodeURIComponent(winId)}`)); @@ -40,6 +42,9 @@ export const backend = { try { const msg = JSON.parse(ev.data); if (msg.type === 'state') this.onState?.(msg.state); + else if (typeof msg.type === 'string') { + this.events.dispatchEvent(new CustomEvent(msg.type, { detail: msg })); + } } catch { // ignore malformed frames } @@ -84,6 +89,22 @@ export const backend = { return data as OpenResult; }, + /* ——— collaboration ——— */ + syncShare: (docId: string): Promise => post(`/api/docs/${docId}/sync/share`), + syncInvite: (docId: string, did: string, role: string): Promise<{ link: string }> => + post(`/api/docs/${docId}/sync/invite`, { did, role }), + syncAttach: (docId: string): Promise< + { shared: boolean; me?: SyncMember; roster?: SyncMember[]; buffered?: SyncFrame[] } + > => post(`/api/docs/${docId}/sync/attach`), + syncSend: (docId: string, b64: string): Promise<{ seq: number }> => + post(`/api/docs/${docId}/sync/send`, { b64 }), + syncEphemeral: (docId: string, b64: string): Promise => + post(`/api/docs/${docId}/sync/ephemeral`, { b64 }), + acceptInvitation: (token: string): Promise => post(`/api/invitations/${token}/accept`), + dismissInvitation: (token: string): Promise => post(`/api/invitations/${token}/dismiss`), + atSignIn: (identifier: string): Promise<{ url: string }> => post('/api/at/signin', { identifier }), + atSignOut: (): Promise => post('/api/at/signout'), + loadData: async (docId: string, name: string): Promise => { const res = await fetch(`/api/docs/${docId}/data/${name}`, { headers: authHeaders() }); if (res.status === 404) return null; diff --git a/src/state/desk.ts b/src/state/desk.ts index aedd9d7..e18b576 100644 --- a/src/state/desk.ts +++ b/src/state/desk.ts @@ -3,7 +3,7 @@ // (?win=). The server owns documents, models, recents, AND window // layout; this store mirrors snapshots and forwards actions. -import type { DocInfo, DocWindowRec, ModelEntry, RecentEntry, ServerState } from '../types.ts'; +import type { DocInfo, DocWindowRec, ModelEntry, PendingInvitation, RecentEntry, ServerState, SyncState } from '../types.ts'; import { backend, type OpenResult } from './backend.ts'; class DeskStore extends EventTarget { @@ -14,6 +14,7 @@ class DeskStore extends EventTarget { tileServerPort = 0; desktop = false; connected = false; + sync: SyncState | undefined; /** Which window this page renders. */ winId: string = 'base'; @@ -33,6 +34,7 @@ class DeskStore extends EventTarget { this.windows = state.windows; this.tileServerPort = state.tileServerPort; this.desktop = state.desktop; + this.sync = state.sync; this.connected = true; this.docs = new Map(state.open.map((d) => [d.id, d])); if (this.winId !== 'base') { @@ -254,6 +256,75 @@ class DeskStore extends EventTarget { } } + /* ——— collaboration ——— */ + + /** Live-session info for a doc, if it is shared. */ + syncDoc(docId: string) { + return this.sync?.docs?.[docId]; + } + + async startSharing(docId: string): Promise { + try { + await backend.syncShare(docId); + this.toast('This document is now live — invite people from this panel'); + } catch (err) { + this.toast(`Could not start sharing: ${(err as Error).message}`); + } + } + + /** Returns the share link on success (also toasts it). */ + async invite(docId: string, did: string, role: string): Promise { + try { + const { link } = await backend.syncInvite(docId, did, role); + try { + await navigator.clipboard.writeText(link); + this.toast('Invitation sent — share link copied to your clipboard'); + } catch { + this.toast(`Invitation sent — share link: ${link}`); + } + return link; + } catch (err) { + this.toast(`Could not invite: ${(err as Error).message}`); + return undefined; + } + } + + async acceptInvitation(inv: PendingInvitation): Promise { + try { + // Download first, verify, then the window opens on the live session. + this.handleOpened(await backend.acceptInvitation(inv.token)); + this.toast(`“${inv.title}” is yours now — synced live with the others`); + } catch (err) { + this.toast(`Could not accept: ${(err as Error).message}`); + } + } + + async dismissInvitation(inv: PendingInvitation): Promise { + try { + await backend.dismissInvitation(inv.token); + } catch (err) { + this.toast((err as Error).message); + } + } + + async atSignIn(identifier: string): Promise { + try { + await backend.atSignIn(identifier); + this.toast('Continue in your browser — Ziran opened your sign-in page'); + } catch (err) { + this.toast(`Could not start sign-in: ${(err as Error).message}`); + } + } + + async atSignOut(): Promise { + try { + await backend.atSignOut(); + this.toast('Signed out — back to this machine’s local identity'); + } catch (err) { + this.toast((err as Error).message); + } + } + toast(message: string) { this.dispatchEvent(new CustomEvent('toast', { detail: message })); } diff --git a/src/sync/atproto.test.ts b/src/sync/atproto.test.ts new file mode 100644 index 0000000..8fc73b8 --- /dev/null +++ b/src/sync/atproto.test.ts @@ -0,0 +1,151 @@ +// Offline verification of the atproto identity layer: multikey round-trips, +// identifier normalization, service-auth JWT mint/verify against synthetic +// DID documents on all three curves, tamper/expiry/audience rejection, and +// the relay's composite door (dev HMAC + atproto JWT side by side). +// Run: deno run -A src/sync/atproto.test.ts + +import { ed25519 } from '@noble/curves/ed25519.js'; +import { p256 } from '@noble/curves/nist.js'; +import { secp256k1 } from '@noble/curves/secp256k1.js'; +import { + type AtCurve, + type DidDocument, + encodeMultikey, + decodeMultikey, + mintServiceAuthJwt, + normalizeIdentifier, + verifyServiceAuthJwt, +} from './atproto.ts'; +import { AtprotoVerifier, CompositeVerifier, DevVerifier, RELAY_LXM } from '../../relay/auth.ts'; +import { startRelay } from '../../relay/main.ts'; + +let failures = 0; +const check = (label: string, ok: boolean, detail = '') => { + console.log(`${ok ? 'PASS' : 'FAIL'} ${label}${detail ? ` — ${detail}` : ''}`); + if (!ok) failures++; +}; + +/* ——— identifier normalization ——— */ +check('normalizes @handle', normalizeIdentifier('@Robin.Berjon.com').value === 'robin.berjon.com'); +check('normalizes at:// form', normalizeIdentifier('at://robin.berjon.com').kind === 'handle'); +check('accepts did:plc', normalizeIdentifier('did:plc:abc123').kind === 'did'); +check('rejects garbage', (() => { + try { + normalizeIdentifier('not a handle!'); + return false; + } catch { + return true; + } +})()); + +/* ——— multikey round-trip ——— */ +const edPriv = ed25519.utils.randomSecretKey(); +const edPub = ed25519.getPublicKey(edPriv); +const mk = encodeMultikey('ed25519', edPub); +const decoded = decodeMultikey(mk); +check('multikey round-trips', decoded.curve === 'ed25519' && decoded.keyBytes.every((b, i) => b === edPub[i])); + +/* ——— service-auth on all three curves ——— */ +const RELAY_DID = 'did:web:ziran.space'; + +function makeIdentity(curve: AtCurve): { did: string; doc: DidDocument; secret: Uint8Array } { + let secret: Uint8Array, pub: Uint8Array; + if (curve === 'ed25519') { + secret = ed25519.utils.randomSecretKey(); + pub = ed25519.getPublicKey(secret); + } else if (curve === 'p256') { + secret = p256.utils.randomSecretKey(); + pub = p256.getPublicKey(secret, true); + } else { + secret = secp256k1.utils.randomSecretKey(); + pub = secp256k1.getPublicKey(secret, true); + } + const did = `did:plc:test${curve}`; + const doc: DidDocument = { + id: did, + alsoKnownAs: [`at://${curve}.test`], + verificationMethod: [{ id: `${did}#atproto`, type: 'Multikey', publicKeyMultibase: encodeMultikey(curve, pub) }], + service: [{ id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://pds.test' }], + }; + return { did, doc, secret }; +} + +const identities = new Map(); +const resolveDid = (did: string) => { + const doc = identities.get(did); + return doc ? Promise.resolve(doc) : Promise.reject(new Error(`unknown ${did}`)); +}; + +for (const curve of ['ed25519', 'p256', 'secp256k1'] as AtCurve[]) { + const id = makeIdentity(curve); + identities.set(id.did, id.doc); + const token = mintServiceAuthJwt( + { iss: id.did, aud: RELAY_DID, exp: Math.floor(Date.now() / 1000) + 60, lxm: RELAY_LXM }, + curve, + id.secret, + ); + const verified = await verifyServiceAuthJwt(token, { aud: RELAY_DID, lxm: RELAY_LXM, resolveDid }) + .catch((e) => `ERR:${e.message}`); + check(`${curve} service-auth JWT verifies`, verified === id.did, String(verified)); +} + +/* ——— rejection paths ——— */ +const alice = makeIdentity('secp256k1'); +identities.set(alice.did, alice.doc); +const good = () => ({ iss: alice.did, aud: RELAY_DID, exp: Math.floor(Date.now() / 1000) + 60, lxm: RELAY_LXM }); + +const expired = mintServiceAuthJwt({ ...good(), exp: Math.floor(Date.now() / 1000) - 120 }, 'secp256k1', alice.secret); +check('expired token rejected', await verifyServiceAuthJwt(expired, { aud: RELAY_DID, resolveDid }).then(() => false, () => true)); + +const wrongAud = mintServiceAuthJwt({ ...good(), aud: 'did:web:evil.example' }, 'secp256k1', alice.secret); +check('wrong-audience token rejected', await verifyServiceAuthJwt(wrongAud, { aud: RELAY_DID, resolveDid }).then(() => false, () => true)); + +const wrongLxm = mintServiceAuthJwt({ ...good(), lxm: 'com.example.other' }, 'secp256k1', alice.secret); +check('wrong-lxm token rejected', await verifyServiceAuthJwt(wrongLxm, { aud: RELAY_DID, lxm: RELAY_LXM, resolveDid }).then(() => false, () => true)); + +const mallory = makeIdentity('secp256k1'); // not in the directory-of-record +const forged = mintServiceAuthJwt(good(), 'secp256k1', mallory.secret); // alice's iss, mallory's key +check('forged signature rejected', await verifyServiceAuthJwt(forged, { aud: RELAY_DID, resolveDid }).then(() => false, () => true)); + +const tampered = (() => { + const t = mintServiceAuthJwt(good(), 'secp256k1', alice.secret).split('.'); + const payload = JSON.parse(atob(t[1]!.replace(/-/g, '+').replace(/_/g, '/'))); + payload.iss = 'did:plc:someoneelse'; + identities.set('did:plc:someoneelse', alice.doc); + t[1] = btoa(JSON.stringify(payload)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + return t.join('.'); +})(); +check('tampered payload rejected', await verifyServiceAuthJwt(tampered, { aud: RELAY_DID, resolveDid }).then(() => false, () => true)); + +/* ——— the composite relay door ——— */ +const dataDir = await Deno.makeTempDir({ prefix: 'ziran-at-' }); +const atVerifier = new AtprotoVerifier(RELAY_DID, resolveDid); +const composite = new CompositeVerifier(atVerifier, new DevVerifier('at-test-secret')); +const relay = await startRelay({ port: 0, dataDir, verifier: composite }); + +async function tryConnect(token: string): Promise { + const ws = new WebSocket(`ws://127.0.0.1:${relay.port}/ws?token=${encodeURIComponent(token)}`); + return await new Promise((resolve) => { + ws.addEventListener('open', () => { + ws.close(); + resolve(true); + }); + ws.addEventListener('error', () => resolve(false)); + }); +} + +const atToken = mintServiceAuthJwt(good(), 'secp256k1', alice.secret); +check('relay admits a valid atproto service-auth JWT', await tryConnect(atToken)); +check('relay admits a dev HMAC token', await tryConnect(await new DevVerifier('at-test-secret').mint('did:ziran:dev'))); +check('relay refuses a forged JWT', !(await tryConnect(mintServiceAuthJwt(good(), 'secp256k1', mallory.secret)))); +check('relay refuses garbage', !(await tryConnect('nope'))); + +relay.shutdown(); +await new Promise((r) => setTimeout(r, 100)); +await Deno.remove(dataDir, { recursive: true }).catch(() => {}); + +if (failures) { + console.error(`\n${failures} failure(s)`); + Deno.exit(1); +} +console.log('\natproto identity layer: all good'); diff --git a/src/sync/atproto.ts b/src/sync/atproto.ts new file mode 100644 index 0000000..5984f0d --- /dev/null +++ b/src/sync/atproto.ts @@ -0,0 +1,304 @@ +// atproto identity utilities: handle/DID resolution, Multikey decoding, and +// service-auth JWT mint/verify. Adapted from matey's SDK-free client modules +// (~/Code/robin.berjon.com/matey — same author, same conventions), trimmed to +// what ziran needs and made fetch-injectable for offline tests. +// +// The service-auth JWT is atproto's inter-service credential: a ~60s compact +// JWS signed by the *user's* repo signing key (the `#atproto` verification +// method in their DID document). Anyone can verify one by resolving the DID +// document — no shared secret, no OAuth infrastructure at the verifier. + +import { ed25519 } from '@noble/curves/ed25519.js'; +import { p256 } from '@noble/curves/nist.js'; +import { secp256k1 } from '@noble/curves/secp256k1.js'; +import { base58 } from '@scure/base'; + +export const DEFAULT_HANDLE_RESOLVER = 'https://public.api.bsky.app'; +export const DEFAULT_PLC_DIRECTORY = 'https://plc.directory'; + +export interface DidService { + id: string; + type: string; + serviceEndpoint: unknown; +} + +export interface DidVerificationMethod { + id: string; + type: string; + controller?: string; + publicKeyMultibase?: string; +} + +export interface DidDocument { + id: string; + alsoKnownAs?: string[]; + verificationMethod?: DidVerificationMethod[]; + service?: DidService[]; +} + +export interface ResolverOptions { + fetch?: typeof fetch; + handleResolver?: string; + plcDirectory?: string; +} + +export interface AtIdentity { + did: string; + document: DidDocument; + handle: string | null; + pds: string | null; +} + +const strip = (s: string) => s.replace(/\/+$/, ''); + +/** Normalize user input: `@handle`, `at://handle`, `handle`, or a DID. */ +export function normalizeIdentifier(input: string): { kind: 'did' | 'handle'; value: string } { + let s = input.trim().toLowerCase(); + if (s.startsWith('at://')) s = s.slice(5); + if (s.startsWith('@')) s = s.slice(1); + if (s.startsWith('did:')) { + if (!/^did:(plc|web):[a-z0-9._:%-]+$/.test(s)) throw new Error(`unsupported DID: ${s}`); + return { kind: 'did', value: s }; + } + if (!/^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]([a-z0-9-]*[a-z0-9])?$/.test(s)) { + throw new Error(`“${input}” is not a handle or DID`); + } + return { kind: 'handle', value: s }; +} + +async function fetchJson(f: typeof fetch, url: string): Promise<{ status: number; json: unknown }> { + const res = await f(url, { headers: { accept: 'application/json' } }); + let json: unknown = null; + try { + json = await res.json(); + } catch { + // non-JSON body + } + return { status: res.status, json }; +} + +export async function resolveHandle(handle: string, opts: ResolverOptions = {}): Promise { + const f = opts.fetch ?? fetch; + const base = strip(opts.handleResolver ?? DEFAULT_HANDLE_RESOLVER); + const { status, json } = await fetchJson( + f, + `${base}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`, + ); + const did = (json as Record | null)?.did; + if (status !== 200 || typeof did !== 'string') { + throw new Error(`could not resolve handle ${handle} (HTTP ${status})`); + } + return did; +} + +export async function resolveDidDocument(did: string, opts: ResolverOptions = {}): Promise { + const f = opts.fetch ?? fetch; + let url: string; + if (did.startsWith('did:plc:')) { + url = `${strip(opts.plcDirectory ?? DEFAULT_PLC_DIRECTORY)}/${did}`; + } else if (did.startsWith('did:web:')) { + const host = decodeURIComponent(did.slice('did:web:'.length)); + if (host.includes('/') || host.includes('?') || host.includes('#')) { + throw new Error('atproto did:web identifiers are hostname-only'); + } + url = `https://${host}/.well-known/did.json`; + } else { + throw new Error(`unsupported DID method: ${did}`); + } + const { status, json } = await fetchJson(f, url); + if (status !== 200 || json === null || typeof json !== 'object') { + throw new Error(`could not resolve ${did} (HTTP ${status})`); + } + const doc = json as DidDocument; + if (doc.id !== did) throw new Error(`DID document id ${doc.id} does not match ${did}`); + return doc; +} + +export function handleFromDocument(doc: DidDocument): string | null { + for (const aka of doc.alsoKnownAs ?? []) { + if (aka.startsWith('at://')) { + const rest = aka.slice(5); + if (rest.length > 0 && !rest.includes('/')) return rest; + } + } + return null; +} + +export function pdsFromDocument(doc: DidDocument): string | null { + for (const svc of doc.service ?? []) { + const id = svc.id.startsWith('#') ? svc.id : `#${svc.id.split('#')[1] ?? ''}`; + if (id === '#atproto_pds' && svc.type === 'AtprotoPersonalDataServer') { + return typeof svc.serviceEndpoint === 'string' ? svc.serviceEndpoint : null; + } + } + return null; +} + +/** Resolve a handle or DID to a full identity, with backlink verification + when starting from a handle. */ +export async function resolveIdentity(identifier: string, opts: ResolverOptions = {}): Promise { + const norm = normalizeIdentifier(identifier); + const did = norm.kind === 'did' ? norm.value : await resolveHandle(norm.value, opts); + const document = await resolveDidDocument(did, opts); + const declared = handleFromDocument(document); + if (norm.kind === 'handle' && declared !== norm.value) { + throw new Error( + `handle verification failed: ${norm.value} resolved to ${did}, whose document declares ${declared ?? 'no handle'}`, + ); + } + return { did, document, handle: declared, pds: pdsFromDocument(document) }; +} + +/* ——— Multikey (did:key material in DID documents) ——— */ + +export type AtCurve = 'ed25519' | 'p256' | 'secp256k1'; + +const PREFIXES: Record = { + ed25519: new Uint8Array([0xed, 0x01]), + p256: new Uint8Array([0x80, 0x24]), + secp256k1: new Uint8Array([0xe7, 0x01]), +}; +const KEY_LENGTHS: Record = { ed25519: 32, p256: 33, secp256k1: 33 }; + +export function decodeMultikey(multibase: string): { curve: AtCurve; keyBytes: Uint8Array } { + if (!multibase.startsWith('z')) throw new Error('expected base58btc multibase (z…)'); + const bytes = base58.decode(multibase.slice(1)); + for (const curve of Object.keys(PREFIXES) as AtCurve[]) { + const prefix = PREFIXES[curve]; + if (bytes.length === prefix.length + KEY_LENGTHS[curve] && prefix.every((b, i) => bytes[i] === b)) { + return { curve, keyBytes: bytes.slice(prefix.length) }; + } + } + throw new Error('unsupported or malformed Multikey'); +} + +export function encodeMultikey(curve: AtCurve, keyBytes: Uint8Array): string { + const prefix = PREFIXES[curve]; + if (keyBytes.length !== KEY_LENGTHS[curve]) { + throw new Error(`bad ${curve} key length ${keyBytes.length}`); + } + const joined = new Uint8Array(prefix.length + keyBytes.length); + joined.set(prefix, 0); + joined.set(keyBytes, prefix.length); + return `z${base58.encode(joined)}`; +} + +/** The repo signing key (`#atproto` verification method) from a DID doc. */ +export function atprotoSigningKey(doc: DidDocument): { curve: AtCurve; keyBytes: Uint8Array } { + for (const vm of doc.verificationMethod ?? []) { + const id = vm.id.startsWith('#') ? vm.id : `#${vm.id.split('#')[1] ?? ''}`; + if (id === '#atproto' && vm.publicKeyMultibase) return decodeMultikey(vm.publicKeyMultibase); + } + throw new Error('DID document has no #atproto signing key'); +} + +/* ——— service-auth JWTs ——— */ + +const te = new TextEncoder(); +const td = new TextDecoder(); + +function b64urlEncode(bytes: Uint8Array): string { + let bin = ''; + for (const b of bytes) bin += String.fromCharCode(b); + return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} +function b64urlDecode(s: string): Uint8Array { + const pad = s.replace(/-/g, '+').replace(/_/g, '/'); + const bin = atob(pad + '='.repeat((4 - (pad.length % 4)) % 4)); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; +} +const b64urlJson = (obj: unknown) => b64urlEncode(te.encode(JSON.stringify(obj))); + +export interface ServiceAuthClaims { + iss: string; + aud: string; + exp: number; + lxm?: string; + jti?: string; +} + +const ALG_FOR_CURVE: Record = { ed25519: 'EdDSA', p256: 'ES256', secp256k1: 'ES256K' }; + +/** Verify a compact-JWS signature against a raw public key. ECDSA signatures + are the JOSE 64-byte r||s form; `lowS: false` because JWA does not require + low-S canonicalization and other stacks emit high-S. */ +export function verifyJwsSignature( + curve: AtCurve, + publicKey: Uint8Array, + signingInput: Uint8Array, + signature: Uint8Array, +): boolean { + switch (curve) { + case 'ed25519': + return ed25519.verify(signature, signingInput, publicKey); + case 'p256': + return p256.verify(signature, signingInput, publicKey, { prehash: true, lowS: false, format: 'compact' }); + case 'secp256k1': + return secp256k1.verify(signature, signingInput, publicKey, { prehash: true, lowS: false, format: 'compact' }); + } +} + +/** Mint a service-auth JWT with a raw signing key. Production tokens come + from the user's PDS (`com.atproto.server.getServiceAuth`); this local + minting exists for tests and for dev identities that own their keys. */ +export function mintServiceAuthJwt( + claims: ServiceAuthClaims, + curve: AtCurve, + secretKey: Uint8Array, +): string { + const h = b64urlJson({ typ: 'JWT', alg: ALG_FOR_CURVE[curve] }); + const p = b64urlJson({ jti: crypto.randomUUID().replace(/-/g, ''), ...claims }); + const input = te.encode(`${h}.${p}`); + let sig: Uint8Array; + switch (curve) { + case 'ed25519': + sig = ed25519.sign(input, secretKey); + break; + case 'p256': + sig = p256.sign(input, secretKey, { prehash: true, lowS: true, format: 'compact' }); + break; + case 'secp256k1': + sig = secp256k1.sign(input, secretKey, { prehash: true, lowS: true, format: 'compact' }); + break; + } + return `${h}.${p}.${b64urlEncode(sig)}`; +} + +export interface VerifyServiceAuthOptions { + /** The service DID this token must be addressed to. */ + aud: string; + /** Required lxm (method binding), if the service enforces one. */ + lxm?: string; + /** DID document lookup; defaults to network resolution. */ + resolveDid?: (did: string) => Promise; + /** Clock skew allowance in seconds. */ + skew?: number; +} + +/** Verify a service-auth JWT end to end: shape, expiry, audience, optional + lxm, and the signature against the issuer's resolved `#atproto` key. + Returns the issuer DID. */ +export async function verifyServiceAuthJwt(token: string, opts: VerifyServiceAuthOptions): Promise { + const parts = token.split('.'); + if (parts.length !== 3) throw new Error('not a compact JWS'); + const [h, p, s] = parts as [string, string, string]; + const header = JSON.parse(td.decode(b64urlDecode(h))) as Record; + const payload = JSON.parse(td.decode(b64urlDecode(p))) as Record; + const iss = payload.iss; + if (typeof iss !== 'string' || !iss.startsWith('did:')) throw new Error('missing iss'); + if (payload.aud !== opts.aud) throw new Error('token is for a different service'); + const exp = payload.exp; + if (typeof exp !== 'number' || exp * 1000 < Date.now() - (opts.skew ?? 30) * 1000) { + throw new Error('token expired'); + } + if (opts.lxm !== undefined && payload.lxm !== opts.lxm) throw new Error('token bound to a different method'); + + const doc = await (opts.resolveDid ?? ((did: string) => resolveDidDocument(did)))(iss); + const { curve, keyBytes } = atprotoSigningKey(doc); + if (header.alg !== ALG_FOR_CURVE[curve]) throw new Error(`alg ${header.alg} does not match the ${curve} key`); + const ok = verifyJwsSignature(curve, keyBytes, te.encode(`${h}.${p}`), b64urlDecode(s)); + if (!ok) throw new Error('bad signature'); + return iss; +} diff --git a/src/sync/e2e.test.ts b/src/sync/e2e.test.ts new file mode 100644 index 0000000..c51c2b6 --- /dev/null +++ b/src/sync/e2e.test.ts @@ -0,0 +1,199 @@ +// Two full Ziran runtimes + one relay, driven over their real HTTP APIs: +// Alice creates a checklist, goes live, invites Bob; Bob sees the invitation +// in his base window state, accepts (download-then-sync), and ops flow both +// ways end-to-end encrypted — the relay log stays ciphertext. +// Run: deno run -A src/sync/e2e.test.ts + +import { join } from '@std/path'; +import { startRelay } from '../../relay/main.ts'; +import { DevVerifier } from '../../relay/auth.ts'; +import { toB64 } from './protocol.ts'; + +let failures = 0; +const check = (label: string, ok: boolean, detail = '') => { + console.log(`${ok ? 'PASS' : 'FAIL'} ${label}${detail ? ` — ${detail}` : ''}`); + if (!ok) failures++; +}; +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); +const enc = (s: string) => new TextEncoder().encode(s); +const dec = (b: Uint8Array) => new TextDecoder().decode(b); + +const tmp = await Deno.makeTempDir({ prefix: 'ziran-e2e-' }); +const relayData = join(tmp, 'relay'); +const relay = await startRelay({ port: 0, dataDir: relayData, verifier: new DevVerifier('e2e-secret') }); +const relayUrl = `ws://127.0.0.1:${relay.port}/ws`; + +interface App { + port: number; + key: string; + child: Deno.ChildProcess; +} + +async function startApp(name: string, did: string, port: number): Promise { + const key = `${name}-key`; + const child = new Deno.Command(Deno.execPath(), { + args: ['run', '-A', 'main.ts'], + env: { + ZIRAN_DATA_DIR: join(tmp, name), + ZIRAN_KEY: key, + ZIRAN_PORT: String(port), + ZIRAN_DID: did, + ZIRAN_HANDLE: `${name}.test`, + ZIRAN_RELAY: relayUrl, + ZIRAN_RELAY_SECRET: 'e2e-secret', + }, + stdout: 'null', + stderr: 'piped', + }).spawn(); + // Wait for the API to come up. + for (let i = 0; i < 60; i++) { + try { + const res = await fetch(`http://127.0.0.1:${port}/api/state`, { headers: { 'x-ziran-key': key } }); + await res.body?.cancel(); + if (res.ok) return { port, key, child }; + } catch { + // not up yet + } + await sleep(250); + } + throw new Error(`${name} did not start`); +} + +const req = async (app: App, path: string, init?: RequestInit) => { + const res = await fetch(`http://127.0.0.1:${app.port}${path}`, { + ...init, + headers: { 'x-ziran-key': app.key, 'content-type': 'application/json', ...(init?.headers ?? {}) }, + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error ?? `${path} → ${res.status}`); + return data; +}; + +let alice: App | undefined; +let bob: App | undefined; +try { + [alice, bob] = await Promise.all([ + startApp('alice', 'did:ziran:e2e-alice', 4901), + startApp('bob', 'did:ziran:e2e-bob', 4902), + ]); + await sleep(1200); // sync managers register with the relay + + // 1. Both runtimes report the relay as reachable, with their identities. + const aState = await req(alice, '/api/state'); + const bState = await req(bob, '/api/state'); + check('alice runtime is relay-connected', aState.sync?.relayOk === true); + check('bob runtime is relay-connected', bState.sync?.relayOk === true); + check('identities honored', aState.sync?.identity?.did === 'did:ziran:e2e-alice'); + + // 2. Alice creates a checklist from the starter model and goes live. + const checklist = aState.models.find((m: { id: string }) => m.id === 'com.berjon.ziran.checklist'); + check('starter checklist model seeded', Boolean(checklist)); + const opened = await req(alice, '/api/docs/from-model', { + method: 'POST', + body: JSON.stringify({ modelId: checklist.id }), + }); + const aDoc = opened.doc.id as string; + await req(alice, `/api/docs/${aDoc}/sync/share`, { method: 'POST' }); + const aState2 = await req(alice, '/api/state'); + check('document is live for alice', Boolean(aState2.sync?.docs?.[aDoc])); + + // 3. Alice appends an op pre-invite (this lands in the pre-join epoch; Bob + // will get its effect through the snapshot, not the log). + const op1 = { op: 'add', id: 'aaaa1111', text: 'pack the telescope' }; + await req(alice, `/api/docs/${aDoc}/sync/send`, { method: 'POST', body: JSON.stringify({ b64: toB64(enc(JSON.stringify(op1))) }) }); + // Simulate the tile persisting after applying the echo: write items data. + const items = JSON.stringify([{ id: 'aaaa1111', text: 'pack the telescope', done: false }]); + await fetch(`http://127.0.0.1:${alice.port}/api/docs/${aDoc}/data/items`, { + method: 'PUT', + headers: { 'x-ziran-key': alice.key }, + body: enc(items), + }); + await sleep(300); + + // 4. Invite Bob. + const invited = await req(alice, `/api/docs/${aDoc}/sync/invite`, { + method: 'POST', + body: JSON.stringify({ did: 'did:ziran:e2e-bob', role: 'editor' }), + }); + check('invite returns a share link', typeof invited.link === 'string' && invited.link.includes('/i/')); + + // 5. The landing page renders for the link. + const landing = await (await fetch(invited.link)).text(); + check('landing page names the document', landing.includes('invited') && landing.includes('ziran://invite/')); + + // 6. Bob's runtime received the invitation (inbox → state snapshot). + await sleep(500); + const bState2 = await req(bob, '/api/state'); + const inv = bState2.sync?.invitations?.[0]; + check('invitation lands in bob’s base window state', Boolean(inv), JSON.stringify(bState2.sync?.invitations)); + check('invitation carries inviter + role', inv?.inviterHandle === 'alice.test' && inv?.role === 'editor'); + check('invitation does not leak the MLS welcome', !('payload' in (inv ?? { payload: 1 }))); + + // 7. Bob accepts: download-then-sync, document opens in a window. + const accepted = await req(bob, `/api/invitations/${inv.token}/accept`, { method: 'POST' }); + const bDoc = accepted.doc.id as string; + check('accept opens a verified document', accepted.doc.damaged === false, accepted.doc.damage); + check('accept created a window', Boolean(accepted.win?.id)); + + // 8. Bob's copy contains alice's pre-invite state (via the snapshot). + const bItems = await fetch(`http://127.0.0.1:${bob.port}/api/docs/${bDoc}/data/items`, { + headers: { 'x-ziran-key': bob.key }, + }); + const bItemsText = bItems.ok ? dec(new Uint8Array(await bItems.arrayBuffer())) : ''; + check('bob’s download carries the pre-invite item', bItemsText.includes('pack the telescope')); + + // 9. Live ops flow alice → bob. + const op2 = { op: 'add', id: 'bbbb2222', text: 'find dark skies' }; + await req(alice, `/api/docs/${aDoc}/sync/send`, { method: 'POST', body: JSON.stringify({ b64: toB64(enc(JSON.stringify(op2))) }) }); + await sleep(400); + const bAttach = await req(bob, `/api/docs/${bDoc}/sync/attach`, { method: 'POST' }); + check('bob is attached to the live session', bAttach.shared === true); + const bGot = (bAttach.buffered ?? []).some((m: { b64: string }) => dec(Uint8Array.from(atob(m.b64.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0))).includes('find dark skies')); + check('alice’s live op reaches bob decrypted', bGot); + + // 10. And bob → alice. + const op3 = { op: 'set', id: 'aaaa1111', done: true }; + await req(bob, `/api/docs/${bDoc}/sync/send`, { method: 'POST', body: JSON.stringify({ b64: toB64(enc(JSON.stringify(op3))) }) }); + await sleep(400); + const aAttach = await req(alice, `/api/docs/${aDoc}/sync/attach`, { method: 'POST' }); + const aGot = (aAttach.buffered ?? []).some((m: { b64: string; author: string }) => m.author === 'did:ziran:e2e-bob'); + check('bob’s op reaches alice, attributed to bob', aGot); + + // 11. Roster shows both members on both sides. + const aFinal = await req(alice, '/api/state'); + const bFinal = await req(bob, '/api/state'); + check('alice sees both members', aFinal.sync?.docs?.[aDoc]?.members?.length === 2); + check('bob sees both members', bFinal.sync?.docs?.[bDoc]?.members?.length === 2); + check('bob’s role is editor', bFinal.sync?.docs?.[bDoc]?.role === 'editor'); + + // 12. THE INVARIANT: the relay stored only ciphertext. + let leaked = false; + const plaintexts = ['pack the telescope', 'find dark skies', 'aaaa1111']; + for await (const doc of Deno.readDir(join(relayData, 'docs'))) { + for (const file of ['log.jsonl', 'snapshot.json']) { + try { + const raw = await Deno.readTextFile(join(relayData, 'docs', doc.name, file)); + // The b64 payloads decode to MLS wire format; check both raw file and + // decoded payloads for plaintext. + if (plaintexts.some((p) => raw.includes(p) || raw.includes(toB64(enc(p))))) leaked = true; + } catch { + // file absent + } + } + } + check('relay log + snapshot are ciphertext only', !leaked); +} finally { + try { + alice?.child.kill(); + bob?.child.kill(); + } catch { /* already gone */ } + relay.shutdown(); + await sleep(200); + await Deno.remove(tmp, { recursive: true }).catch(() => {}); +} + +if (failures) { + console.error(`\n${failures} failure(s)`); + Deno.exit(1); +} +console.log('\ntwo-runtime E2EE collaboration: all good'); diff --git a/src/sync/mls-transport.test.ts b/src/sync/mls-transport.test.ts new file mode 100644 index 0000000..12c1c05 --- /dev/null +++ b/src/sync/mls-transport.test.ts @@ -0,0 +1,156 @@ +// The capstone: end-to-end encrypted group collaboration through the real +// relay. Proves that members exchange decryptable messages, that a late joiner +// catches up from an encrypted snapshot, and — the whole point — that the +// relay's stored log contains only ciphertext, never plaintext. +// Run: deno run -A src/sync/mls-transport.test.ts + +import { join } from '@std/path'; +import { startRelay } from '../../relay/main.ts'; +import { DevVerifier } from '../../relay/auth.ts'; +import { RelaySyncTransport } from './relay-client.ts'; +import { MemoryKeyPackageDirectory, MlsSyncTransport } from './mls-transport.ts'; +import { fromB64 } from './protocol.ts'; +import type { Session, SyncManifest } from './protocol.ts'; + +let failures = 0; +const check = (label: string, ok: boolean, detail = '') => { + console.log(`${ok ? 'PASS' : 'FAIL'} ${label}${detail ? ` — ${detail}` : ''}`); + if (!ok) failures++; +}; +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); +const enc = (s: string) => new TextEncoder().encode(s); +const dec = (b: Uint8Array) => new TextDecoder().decode(b); + +const manifest: SyncManifest = { + owner: 'author', + roles: { + author: { write: true, admin: true }, + editor: { write: true }, + watcher: { write: false }, + }, +}; + +const dataDir = await Deno.makeTempDir({ prefix: 'ziran-mls-' }); +const verifier = new DevVerifier('secret'); +const relay = await startRelay({ port: 0, dataDir, verifier }); +const url = `ws://127.0.0.1:${relay.port}/ws`; +const directory = new MemoryKeyPackageDirectory(); + +const DOC = 'doc-e2ee'; +function make(did: string, handle: string) { + const base = new RelaySyncTransport({ + url, + did, + handle, + token: () => verifier.mint(did), + manifestFor: (d) => (d === DOC ? manifest : undefined), + }); + return new MlsSyncTransport({ base, did, directory }); +} + +// Collect decrypted messages per member for assertions. +function collect(session: Session) { + const msgs: string[] = []; + session.onMessage((m) => msgs.push(dec(m.bytes))); + return msgs; +} + +try { + const alice = make('did:plc:alice', 'alice.test'); + const bob = make('did:plc:bob', 'bob.test'); + const carol = make('did:plc:carol', 'carol.test'); + await Promise.all([alice.register(), bob.register(), carol.register()]); + + // 1. Alice creates the doc (founds the MLS group) and writes one message. + const aSession = await alice.join(DOC); + const aMsgs = collect(aSession); + await aSession.append(enc('alice-1 before invites')); + await sleep(50); + check('owner joins, founds group, self-delivers own message', aMsgs.includes('alice-1 before invites')); + + // 2. Alice invites Bob (MLS add-commit rides the log; welcome in the invite). + await alice.invite(DOC, 'did:plc:bob', 'editor'); + await sleep(50); + // Owner re-snapshots at the new epoch so the joiner can catch up. + const snapshotState = enc('SNAPSHOT: alice-1 applied'); + const atSeq = aSession.roster().length ? 2 : 2; // after 'alice-1'(1) + commit(2) + await aSession.snapshot(snapshotState, atSeq); + await sleep(50); + + // 3. Bob reads his inbox, accepts (joins group + decrypts snapshot), joins. + let bobInvToken = ''; + (async () => { + for await (const inv of bob.inbox()) { + bobInvToken = inv.token; + break; + } + })(); + await sleep(80); + check('bob received the invitation via inbox', bobInvToken !== ''); + const bobAccept = await bob.accept(bobInvToken); + check('bob decrypts the catch-up snapshot', bobAccept.snapshot != null && dec(bobAccept.snapshot!) === 'SNAPSHOT: alice-1 applied'); + const bSession = await bob.join(DOC, { sinceSeq: bobAccept.atSeq }); + const bMsgs = collect(bSession); + await sleep(50); + + // 4. Two-way encrypted messaging after the join. + await aSession.append(enc('alice-2 after bob joined')); + await sleep(80); + check('bob decrypts alice’s post-join message', bMsgs.includes('alice-2 after bob joined')); + await bSession.append(enc('bob-1 reply')); + await sleep(80); + check('alice decrypts bob’s reply', aMsgs.includes('bob-1 reply')); + + // 5. Add a watcher (Carol); she gets a fresh snapshot and reads new traffic. + await alice.invite(DOC, 'did:plc:carol', 'watcher'); + await sleep(50); + await aSession.snapshot(enc('SNAPSHOT: two messages'), 5); + await sleep(50); + let carolTok = ''; + (async () => { + for await (const inv of carol.inbox()) { + carolTok = inv.token; + break; + } + })(); + await sleep(80); + const carolAccept = await carol.accept(carolTok); + check('watcher decrypts her snapshot', carolAccept.snapshot != null && dec(carolAccept.snapshot!) === 'SNAPSHOT: two messages'); + const cSession = await carol.join(DOC, { sinceSeq: carolAccept.atSeq }); + const cMsgs = collect(cSession); + await sleep(50); + await aSession.append(enc('alice-3 for the watcher')); + await sleep(100); + check('watcher decrypts a post-join message', cMsgs.includes('alice-3 for the watcher')); + + // 6. THE POINT: the relay's stored log is ciphertext only. + await sleep(50); + const logText = await Deno.readTextFile(join(dataDir, 'docs', DOC, 'log.jsonl')); + const entries = logText.split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l) as { b64: string }); + const plaintexts = ['alice-1 before invites', 'alice-2 after bob joined', 'bob-1 reply', 'alice-3 for the watcher']; + let leaked = false; + for (const e of entries) { + const raw = dec(fromB64(e.b64)); + if (plaintexts.some((p) => raw.includes(p))) leaked = true; + } + check('relay log contains ciphertext only (no plaintext leaked)', !leaked, `${entries.length} log entries scanned`); + + // 7. And the snapshot blob on disk is ciphertext too. + const snapText = await Deno.readTextFile(join(dataDir, 'docs', DOC, 'snapshot.json')); + const snapRaw = dec(fromB64((JSON.parse(snapText) as { b64: string }).b64)); + check('relay snapshot blob is ciphertext', !snapRaw.includes('SNAPSHOT:')); + + alice.close(); + bob.close(); + carol.close(); + relay.shutdown(); +} finally { + await sleep(100); + await Deno.remove(dataDir, { recursive: true }).catch(() => {}); +} + +if (failures) { + console.error(`\n${failures} failure(s)`); + Deno.exit(1); +} +console.log('\nE2EE group sync through the relay: all good'); diff --git a/src/sync/mls-transport.ts b/src/sync/mls-transport.ts new file mode 100644 index 0000000..eed8145 --- /dev/null +++ b/src/sync/mls-transport.ts @@ -0,0 +1,310 @@ +// The E2EE transport: wraps the plaintext RelaySyncTransport so the relay only +// ever sees MLS ciphertext. Durable log messages are MLS application messages +// (per-message forward secrecy); membership commits ride the same log and are +// processed by every member; the invitation carries the MLS Welcome so the +// invitee can join the group; snapshots are encrypted for the current epoch so +// a late joiner can catch up. MLS lives host-side only — never in tiles. +// +// KeyPackages are discovered through a directory (a per-user PDS record in +// production, Germ-style; in-memory for tests). Rights stay two-layer: the +// relay ACL governs *write*, MLS group membership governs *read*. + +import { + type AcceptResult, + type EphemeralMessage, + fromB64, + type Invitation, + type JoinOptions, + type LogMessage, + type Member, + type Session, + type SyncTransport, + toB64, +} from './protocol.ts'; +import { createIdentity, encodeKeyPackage, type Identity, MlsGroup } from './mls.ts'; +import type { RelaySyncTransport } from './relay-client.ts'; + +/** Where members publish and discover MLS KeyPackages. Production: a record in + each user's own PDS (Germ pattern), fetched by resolving their DID. */ +export interface KeyPackageDirectory { + publish(did: string, keyPackageWire: Uint8Array): Promise; + /** Fetch a member's KeyPackage; throws if the DID has none. */ + fetch(did: string): Promise; +} + +/** A trivial shared directory for tests / a single-process runtime. */ +export class MemoryKeyPackageDirectory implements KeyPackageDirectory { + #map = new Map(); + publish(did: string, kp: Uint8Array): Promise { + this.#map.set(did, kp); + return Promise.resolve(); + } + fetch(did: string): Promise { + const kp = this.#map.get(did); + if (!kp) return Promise.reject(new Error(`no KeyPackage published for ${did}`)); + return Promise.resolve(kp); + } +} + +/** The relay-hosted directory: stands in for per-user PDS records until AT + sign-in lands; the discovery seam stays the same either way. */ +export class RelayKeyPackageDirectory implements KeyPackageDirectory { + constructor(private relay: RelaySyncTransport) {} + publish(_did: string, kp: Uint8Array): Promise { + return this.relay.publishKeyPackage(kp); + } + async fetch(did: string): Promise { + const kp = await this.relay.getKeyPackage(did); + if (!kp) throw new Error(`${did} has not published collaboration keys yet — they need to run Ziran once`); + return kp; + } +} + +/** Durable storage for serialized MLS group state, so shared documents keep + decrypting across app restarts. */ +export interface GroupStateStore { + load(syncDocId: string): Promise; + save(syncDocId: string, state: Uint8Array): Promise; +} + +class MlsSession implements Session { + #msg = new Set<(m: LogMessage) => void>(); + #eph = new Set<(m: EphemeralMessage) => void>(); + + constructor( + readonly docId: string, + private base: Session, + private group: MlsGroup, + private selfDid: string, + private dirty: () => void = () => {}, + ) { + base.onMessage(async (m) => { + if (m.author === this.selfDid) return; // our own echo; delivered on append + let res; + try { + res = await this.group.process(m.bytes); + } catch { + // Pre-join history: a late joiner's replay contains commits for + // epochs it skipped and app messages forward secrecy locks away. + // Both are expected and superseded by the snapshot — drop them. + return; + } + this.dirty(); + if (res.kind === 'application') { + for (const h of this.#msg) h({ seq: m.seq, author: m.author, role: m.role, bytes: res.plaintext }); + } + // membership commits advance the epoch silently; roster comes from base + }); + base.onEphemeral(async (m) => { + if (m.author === this.selfDid) return; + let res; + try { + res = await this.group.process(m.bytes); + } catch { + return; // stale presence from an epoch we can't read — drop + } + this.dirty(); + if (res.kind === 'application') { + for (const h of this.#eph) h({ author: m.author, role: m.role, bytes: res.plaintext }); + } + }); + } + + get me(): Member { + return this.base.me; + } + roster() { + return this.base.roster(); + } + onRoster(h: (r: Member[]) => void) { + return this.base.onRoster(h); + } + + async append(bytes: Uint8Array): Promise { + const cipher = await this.group.encrypt(bytes); + this.dirty(); + const seq = await this.base.append(cipher); + // Deliver our own message to local handlers in order (MLS can't decrypt + // its own application messages, so the echo is skipped above). + for (const h of this.#msg) h({ seq, author: this.selfDid, role: this.me.role, bytes }); + return seq; + } + + async ephemeral(bytes: Uint8Array): Promise { + const cipher = await this.group.encrypt(bytes); + this.dirty(); + this.base.ephemeral(cipher); + } + + async snapshot(bytes: Uint8Array, atSeq: number): Promise { + // Encrypted for the current epoch; a member joining at this epoch can + // decrypt it, then replay the tail after atSeq. + const cipher = await this.group.encrypt(bytes); + this.dirty(); + await this.base.snapshot(cipher, atSeq); + } + + onMessage(h: (m: LogMessage) => void) { + this.#msg.add(h); + return () => this.#msg.delete(h); + } + onEphemeral(h: (m: EphemeralMessage) => void) { + this.#eph.add(h); + return () => this.#eph.delete(h); + } + leave() { + this.base.leave(); + } + + /** The engine, exposed so the transport can add members and re-snapshot. */ + _group(): MlsGroup { + return this.group; + } + + /** Append an already-MLS message (a commit) to the log without re-encrypting. */ + base_append_raw(raw: Uint8Array): Promise { + return this.base.append(raw); + } +} + +export interface MlsTransportOptions { + base: RelaySyncTransport; + did: string; + directory: KeyPackageDirectory; + /** MLS group id for a document (defaults to the docId bytes). */ + groupIdFor?: (docId: string) => Uint8Array; + /** When set, group state persists across restarts (debounced after every + ratchet advance — encryption included, since reusing a generation would + reuse a nonce). */ + groupStore?: GroupStateStore; +} + +export class MlsSyncTransport implements SyncTransport { + #identity?: Identity; + #groups = new Map(); + #sessions = new Map(); + #invitations = new Map(); // token → full invite (with welcome) + #published = false; + #saveTimers = new Map(); + + constructor(private opts: MlsTransportOptions) {} + + /** Debounced group-state persistence; called after every ratchet advance. */ + #dirty(docId: string): void { + const store = this.opts.groupStore; + if (!store) return; + clearTimeout(this.#saveTimers.get(docId)); + this.#saveTimers.set( + docId, + setTimeout(() => { + const group = this.#groups.get(docId); + if (group) store.save(docId, group.serialize()).catch(() => {}); + }, 80) as unknown as number, + ); + } + + /** Restore a persisted group before join() (a restart of a shared doc). */ + async restoreGroup(docId: string): Promise { + if (this.#groups.has(docId)) return true; + const bytes = await this.opts.groupStore?.load(docId); + if (!bytes) return false; + this.#groups.set(docId, await MlsGroup.restore(bytes, this.opts.did)); + return true; + } + + /** Ensure this member has an MLS identity and its KeyPackage is published + to the directory (their PDS). Call once at startup/login — an invitee + must be discoverable before anyone can add them to a group. */ + async register(): Promise { + await this.#identityReady(); + } + + async #identityReady(): Promise { + if (!this.#identity) this.#identity = await createIdentity(this.opts.did); + if (!this.#published) { + this.#published = true; + await this.opts.directory.publish(this.opts.did, encodeKeyPackage(this.#identity.publicPackage)); + } + return this.#identity; + } + + #groupId(docId: string): Uint8Array { + return this.opts.groupIdFor?.(docId) ?? new TextEncoder().encode(docId); + } + + async join(docId: string, opts?: JoinOptions): Promise { + const id = await this.#identityReady(); + let group = this.#groups.get(docId); + if (!group) { + // No group yet: this must be the owner creating the document (an invitee + // gets its group from accept(); a restart from restoreGroup()). + group = await MlsGroup.create(this.#groupId(docId), id); + this.#groups.set(docId, group); + this.#dirty(docId); + } + const base = await this.opts.base.join(docId, opts); + const session = new MlsSession(docId, base, group, this.opts.did, () => this.#dirty(docId)); + this.#sessions.set(docId, session); + return session; + } + + async invite(docId: string, inviteeDid: string, role: string, title?: string): Promise { + const session = this.#sessions.get(docId); + const group = this.#groups.get(docId); + if (!session || !group) throw new Error('join the document before inviting'); + // Fetch the invitee's published KeyPackage and add them to the group. + const kp = await this.opts.directory.fetch(inviteeDid); + const { commit, welcome } = await group.addMember(kp); + this.#dirty(docId); + // The commit rides the ordered log so current members advance their epoch; + // it is a raw MLS message, appended without re-encryption. + await session.base_append_raw(commit); + // The Welcome travels inside the invitation (opaque to the relay). + return this.opts.base.invite(docId, inviteeDid, role, { + title: title ?? docId, + payload: toB64(welcome), + }); + } + + async *inbox(): AsyncIterable { + for await (const inv of this.opts.base.inbox()) { + this.#invitations.set(inv.token, inv); + yield inv; + } + } + + async accept(token: string): Promise { + const id = await this.#identityReady(); + // Deep-link accepts may arrive before (or without) the inbox stream; the + // relay hands the invitation to its named invitee on request. + let inv = this.#invitations.get(token); + if (!inv?.payload) inv = (await this.opts.base.getInvite(token)) ?? undefined; + if (!inv?.payload) throw new Error('invitation not found — it may have been withdrawn'); + // Join the MLS group from the Welcome, then claim relay membership. + const group = await MlsGroup.join(fromB64(inv.payload), id); + this.#groups.set(inv.docId, group); + this.#dirty(inv.docId); + const result = await this.opts.base.accept(token); + // Decrypt the catch-up snapshot (encrypted for our joining epoch). + let snapshot: Uint8Array | null = null; + if (result.snapshot) { + const res = await group.process(result.snapshot); + this.#dirty(inv.docId); + snapshot = res.kind === 'application' ? res.plaintext : null; + } + return { ...result, snapshot }; + } + + /** The invitation cache (inbox entries seen this run). */ + invitationFor(token: string): Invitation | undefined { + return this.#invitations.get(token); + } + + fetchSnapshot(): Promise<{ bytes: Uint8Array; atSeq: number } | null> { + return Promise.resolve(null); + } + + close() { + this.opts.base.close(); + } +} diff --git a/src/sync/mls.test.ts b/src/sync/mls.test.ts new file mode 100644 index 0000000..bebc9ab --- /dev/null +++ b/src/sync/mls.test.ts @@ -0,0 +1,105 @@ +// The MLS engine in isolation (no relay): identity minting, group creation, +// adding a member via commit + welcome, encrypted application round-trips in +// both directions, forward secrecy for late joiners, and that an outsider +// cannot decrypt. Run: deno run -A src/sync/mls.test.ts + +import { createIdentity, encodeKeyPackage, MlsGroup } from './mls.ts'; + +let failures = 0; +const check = (label: string, ok: boolean, detail = '') => { + console.log(`${ok ? 'PASS' : 'FAIL'} ${label}${detail ? ` — ${detail}` : ''}`); + if (!ok) failures++; +}; +const enc = (s: string) => new TextEncoder().encode(s); +const dec = (b: Uint8Array) => new TextDecoder().decode(b); + +const ALICE = 'did:plc:alice'; +const BOB = 'did:plc:bob'; +const CAROL = 'did:plc:carol'; + +const alice = await createIdentity(ALICE); +const bob = await createIdentity(BOB); +const carol = await createIdentity(CAROL); + +const groupId = crypto.getRandomValues(new Uint8Array(16)); + +// 1. Alice founds the group. +const aGroup = await MlsGroup.create(groupId, alice); +check('owner creates a group at epoch 0', aGroup.epoch() === 0n, String(aGroup.epoch())); +check('owner is the sole member', aGroup.members().length === 1 && aGroup.members()[0] === ALICE); + +// 2. Alice adds Bob: commit goes on the log, welcome to Bob. +const add = await aGroup.addMember(encodeKeyPackage(bob.publicPackage)); +check('add advances the epoch', aGroup.epoch() === 1n, String(aGroup.epoch())); +check('roster now has both members', aGroup.members().length === 2 && aGroup.members().includes(BOB)); + +// 3. Bob joins from the welcome; sees the same epoch. +const bGroup = await MlsGroup.join(add.welcome, bob); +check('joiner lands at the same epoch', bGroup.epoch() === aGroup.epoch()); + +// 4. Alice → Bob encrypted application message. +const c1 = await aGroup.encrypt(enc('hello bob, this is private')); +const r1 = await bGroup.process(c1); +check('bob decrypts alice’s message', r1.kind === 'application' && dec(r1.plaintext) === 'hello bob, this is private'); +check('ciphertext does not contain the plaintext', !dec(c1).includes('hello bob')); + +// 5. Bob → Alice, the other direction. +const c2 = await bGroup.encrypt(enc('got it, replying')); +const r2 = await aGroup.process(c2); +check('alice decrypts bob’s reply', r2.kind === 'application' && dec(r2.plaintext) === 'got it, replying'); + +// 6. An outsider (Carol, never added) cannot decrypt. +const carolGroup = await MlsGroup.create(crypto.getRandomValues(new Uint8Array(16)), carol); +let outsiderFailed = false; +try { + const res = await carolGroup.process(c1); + outsiderFailed = res.kind !== 'application'; +} catch { + outsiderFailed = true; +} +check('outsider cannot decrypt the group’s traffic', outsiderFailed); + +// 7. Forward secrecy: Alice adds Carol now; the commit is processed by Bob, +// and Carol (joining at epoch 2) cannot read the epoch-0/1 ciphertext but +// can read new messages. (This is why late-join needs a fresh snapshot.) +const add2 = await aGroup.addMember(encodeKeyPackage(carol.publicPackage)); +await bGroup.process(add2.commit); // Bob applies the membership commit +check('group advanced to epoch 2', aGroup.epoch() === 2n && bGroup.epoch() === 2n); +const cGroup = await MlsGroup.join(add2.welcome, carol); +let carolPreJoin = false; +try { + const res = await cGroup.process(c1); // an old epoch-1 message + carolPreJoin = res.kind === 'application'; +} catch { + carolPreJoin = false; +} +check('late joiner cannot read pre-join history (forward secrecy)', !carolPreJoin); +const c3 = await aGroup.encrypt(enc('welcome carol')); +await bGroup.process(c3); +const r3 = await cGroup.process(c3); +check('late joiner reads post-join messages', r3.kind === 'application' && dec(r3.plaintext) === 'welcome carol'); + +// 8. Persistence: serialize and restore Bob's group, keep decrypting. +const restored = await MlsGroup.restore(bGroup.serialize(), BOB); +const c4 = await aGroup.encrypt(enc('after a restart')); +await cGroup.process(c4); +const r4 = await restored.process(c4); +check('restored group keeps decrypting', r4.kind === 'application' && dec(r4.plaintext) === 'after a restart'); + +// 9. A 2 MiB snapshot-sized payload round-trips (catch-up blobs). +const big = new Uint8Array(2 * 1024 * 1024); +for (let off = 0; off < big.length; off += 65536) { + crypto.getRandomValues(big.subarray(off, Math.min(off + 65536, big.length))); +} +const t0 = performance.now(); +const cBig = await aGroup.encrypt(big); +await cGroup.process(cBig); +const rBig = await bGroup.process(cBig); +const ms = Math.round(performance.now() - t0); +check('2 MiB payload round-trips', rBig.kind === 'application' && (rBig.plaintext.length === big.length), `${ms}ms`); + +if (failures) { + console.error(`\n${failures} failure(s)`); + Deno.exit(1); +} +console.log('\nMLS engine: all good'); diff --git a/src/sync/mls.ts b/src/sync/mls.ts new file mode 100644 index 0000000..e20a7d3 --- /dev/null +++ b/src/sync/mls.ts @@ -0,0 +1,232 @@ +// The MLS engine: a deliberately narrow façade over ts-mls (RFC 9420). Every +// use of the crypto library is confined here, behind +// create-identity / create-group / join / add / encrypt / process — so the +// implementation is swappable (a wasm mls-rs build later) and the rest of the +// runtime, the UI, and tiles never see keys or ciphertext framing. +// +// MLS runs host-side only (the Deno runtime process). Group state is +// immutable in ts-mls — each operation returns a new ClientState — so an +// MlsGroup holds the latest state and replaces it after every op. + +import { + acceptAll, + type ClientState, + type CiphersuiteImpl, + createApplicationMessage, + createCommit, + createGroup, + decodeGroupState, + decodeMlsMessage, + defaultAuthenticationService, + defaultCapabilities, + defaultKeyPackageEqualityConfig, + defaultKeyRetentionConfig, + defaultLifetime, + defaultLifetimeConfig, + defaultPaddingConfig, + emptyPskIndex, + encodeGroupState, + encodeMlsMessage, + generateKeyPackage, + getCiphersuiteFromName, + getCiphersuiteImpl, + joinGroup, + type KeyPackage, + type MLSMessage, + type PrivateKeyPackage, + processMessage, +} from 'ts-mls'; + +// Non-PQ, mandatory-to-implement (RFC 9420) and widely interoperable. Uses +// AES-128-GCM, which @hpke/core provides natively — no extra optional deps to +// bundle (the ChaCha suite Germ uses would pull @hpke/chacha20poly1305). +const SUITE = 'MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519'; + +let suitePromise: Promise | undefined; +function suite(): Promise { + return (suitePromise ??= getCiphersuiteImpl(getCiphersuiteFromName(SUITE))); +} + +const te = new TextEncoder(); +const td = new TextDecoder(); + +// Reassembled from the exported defaults (ts-mls does not re-export +// defaultClientConfig). ClientState = GroupState & { clientConfig }, but only +// GroupState is serialized, so restore() must re-attach this. +const defaultClientConfig = { + keyRetentionConfig: defaultKeyRetentionConfig, + lifetimeConfig: defaultLifetimeConfig, + keyPackageEqualityConfig: defaultKeyPackageEqualityConfig, + paddingConfig: defaultPaddingConfig, + authService: defaultAuthenticationService, +}; + +/** A member's freshly minted MLS credential + KeyPackage. The public package + is what gets published in the member's PDS (Germ pattern); the private + package stays local. */ +export interface Identity { + did: string; + publicPackage: KeyPackage; + privatePackage: PrivateKeyPackage; +} + +/** Result of a membership-changing commit: the commit goes on the ordered + log (every current member processes it); the welcome is delivered to the + new member out-of-band (their inbox). */ +export interface AddResult { + commit: Uint8Array; + welcome: Uint8Array; +} + +/** What processing one incoming wire message yielded. */ +export type Incoming = + | { kind: 'application'; plaintext: Uint8Array } + | { kind: 'membership' } // a commit/proposal was applied; roster may have changed + | { kind: 'ignored' }; // e.g. our own echoed message + +/** Mint an MLS identity for a DID (credential identity = the DID bytes). */ +export async function createIdentity(did: string): Promise { + const cs = await suite(); + const { publicPackage, privatePackage } = await generateKeyPackage( + { credentialType: 'basic', identity: te.encode(did) }, + defaultCapabilities(), + defaultLifetime, + [], + cs, + ); + return { did, publicPackage, privatePackage }; +} + +/** Encode a KeyPackage for publication (to a PDS record / directory). */ +export function encodeKeyPackage(pkg: KeyPackage): Uint8Array { + return encodeMlsMessage({ version: 'mls10', wireformat: 'mls_key_package', keyPackage: pkg }); +} + +/** Decode a KeyPackage fetched from a directory/PDS. */ +export function decodeKeyPackage(bytes: Uint8Array): KeyPackage { + const [msg] = decodeMlsMessage(bytes, 0) as [MLSMessage, number]; + if (msg.wireformat !== 'mls_key_package') throw new Error('not a key package'); + return msg.keyPackage; +} + +function didOfCredential(cred: { credentialType: string; identity?: Uint8Array }): string { + return cred.credentialType === 'basic' && cred.identity ? td.decode(cred.identity) : ''; +} + +export class MlsGroup { + // Every state-advancing operation runs through one chain: two concurrent + // encrypts would otherwise read the same ratchet state and reuse a + // generation (nonce reuse on the sender, replay-rejection at receivers). + #chain: Promise = Promise.resolve(); + + private constructor( + private state: ClientState, + private cs: CiphersuiteImpl, + readonly selfDid: string, + ) {} + + #serial(fn: () => Promise): Promise { + const next = this.#chain.then(fn, fn); + this.#chain = next.catch(() => {}); + return next; + } + + /** Found a new group; the caller is its only (owner) member. */ + static async create(groupId: Uint8Array, id: Identity): Promise { + const cs = await suite(); + const state = await createGroup(groupId, id.publicPackage, id.privatePackage, [], cs); + return new MlsGroup(state, cs, id.did); + } + + /** Join an existing group from a Welcome delivered to us. */ + static async join(welcomeWire: Uint8Array, id: Identity): Promise { + const cs = await suite(); + const [msg] = decodeMlsMessage(welcomeWire, 0) as [MLSMessage, number]; + if (msg.wireformat !== 'mls_welcome') throw new Error('not a welcome'); + const state = await joinGroup(msg.welcome, id.publicPackage, id.privatePackage, emptyPskIndex, cs); + return new MlsGroup(state, cs, id.did); + } + + /** Restore a persisted group. */ + static async restore(stateBytes: Uint8Array, selfDid: string): Promise { + const cs = await suite(); + const [group] = decodeGroupState(stateBytes, 0) as [Omit, number]; + const state = { ...group, clientConfig: defaultClientConfig } as ClientState; + return new MlsGroup(state, cs, selfDid); + } + + /** Serialize group state for durable storage. */ + serialize(): Uint8Array { + return encodeGroupState(this.state); + } + + epoch(): bigint { + return this.state.groupContext.epoch; + } + + /** DIDs of all current members (from their basic-credential identities). */ + members(): string[] { + const out: string[] = []; + for (const n of this.state.ratchetTree) { + if (n != null && n.nodeType === 'leaf') { + const did = didOfCredential(n.leaf.credential); + if (did) out.push(did); + } + } + return out; + } + + /** Add a member by their published KeyPackage: returns the commit (for the + log) and the welcome (for the new member). Advances the epoch. */ + addMember(keyPackageWire: Uint8Array): Promise { + return this.#serial(async () => { + const keyPackage = decodeKeyPackage(keyPackageWire); + const result = await createCommit( + { state: this.state, cipherSuite: this.cs }, + { + extraProposals: [{ proposalType: 'add', add: { keyPackage } }], + // Embed the tree so a relay-delivered Welcome is self-contained (the + // joiner has no side channel to fetch the ratchet tree). + ratchetTreeExtension: true, + }, + ); + if (!result.welcome) throw new Error('add produced no welcome'); + this.state = result.newState; + return { + commit: encodeMlsMessage(result.commit), + welcome: encodeMlsMessage({ version: 'mls10', wireformat: 'mls_welcome', welcome: result.welcome }), + }; + }); + } + + /** Encrypt an application payload → wire bytes for the ordered log. */ + encrypt(plaintext: Uint8Array): Promise { + return this.#serial(async () => { + const { newState, privateMessage } = await createApplicationMessage(this.state, plaintext, this.cs); + this.state = newState; + return encodeMlsMessage({ version: 'mls10', wireformat: 'mls_private_message', privateMessage }); + }); + } + + /** Process an incoming wire message from the log (someone else's). MLS + cannot decrypt our own application messages, so those are handled by the + transport locally and never reach here. */ + process(wire: Uint8Array): Promise { + return this.#serial(async () => { + const [msg] = decodeMlsMessage(wire, 0) as [MLSMessage, number]; + if (msg.wireformat === 'mls_private_message') { + const res = await processMessage(msg, this.state, emptyPskIndex, acceptAll, this.cs); + this.state = res.newState; + return res.kind === 'applicationMessage' + ? { kind: 'application', plaintext: res.message } + : { kind: 'membership' }; + } + if (msg.wireformat === 'mls_public_message') { + const res = await processMessage(msg, this.state, emptyPskIndex, acceptAll, this.cs); + this.state = res.newState; + return { kind: 'membership' }; + } + return { kind: 'ignored' }; + }); + } +} diff --git a/src/sync/protocol.ts b/src/sync/protocol.ts new file mode 100644 index 0000000..4c3a3b6 --- /dev/null +++ b/src/sync/protocol.ts @@ -0,0 +1,161 @@ +// The shared vocabulary between the ziran runtime and the ziran relay, and +// the SyncTransport seam the runtime programs against. Phase 1 has exactly +// one transport (the relay, plaintext); phase 2 wraps payloads in MLS; a +// later Iroh fast path or Matrix adapter would implement the same interface. + +/** A participant as the runtime/relay sees them. Role is a document-defined + string; the runtime and relay only enforce write-permission + membership. */ +export interface Member { + did: string; + handle?: string; + role: string; +} + +/** A role's runtime-enforceable capabilities, declared in the tile manifest + under `sync.roles`. Everything else about what a role *means* is the + tile's business. */ +export interface RoleSpec { + /** May append durable messages to the log. */ + write?: boolean; + /** May send on the ephemeral presence lane. */ + presence?: boolean; + /** Roles whose presence/roster entries this role is allowed to see. + Absent = sees everyone; [] = sees only itself. */ + seesRoles?: string[]; + /** Max simultaneous members holding this role (e.g. one `opponent`). */ + max?: number; + /** May invite others / assign roles / remove members. */ + admin?: boolean; +} + +export interface SyncManifest { + /** The role granted to the document's creator/owner. */ + owner: string; + roles: Record; + /** Role given to someone who opens a public link without a named invite, + if any (absent = invite-only). */ + publicRole?: string; +} + +export interface Invitation { + token: string; + docId: string; + /** Human label for the document, shown in the invitee's inbox. */ + title: string; + inviterDid: string; + inviterHandle?: string; + inviteeDid: string; + role: string; + createdAt: number; + /** Opaque to the relay: the MLS Welcome (b64) under E2EE transports, so the + invitee can join the group. Absent for plaintext transports. */ + payload?: string; +} + +/** Durable, ordered, attributed message delivered to a joined session. */ +export interface LogMessage { + seq: number; + author: string; // DID, relay-attested + role: string; + /** Opaque payload (plaintext in phase 1, MLS ciphertext in phase 2). */ + bytes: Uint8Array; +} + +export interface EphemeralMessage { + author: string; + role: string; + bytes: Uint8Array; +} + +/** A live attachment to one document's channel. */ +export interface Session { + docId: string; + me: Member; + roster(): Member[]; + /** Append to the durable log; resolves with the assigned seq. */ + append(bytes: Uint8Array): Promise; + ephemeral(bytes: Uint8Array): void; + /** Upload the current document state as the catch-up snapshot, tagged with + the log seq it reflects. */ + snapshot(bytes: Uint8Array, atSeq: number): Promise; + onMessage(handler: (m: LogMessage) => void): () => void; + onEphemeral(handler: (m: EphemeralMessage) => void): () => void; + onRoster(handler: (roster: Member[]) => void): () => void; + leave(): void; +} + +export interface JoinOptions { + /** Replay durable messages with seq greater than this (after loading a + snapshot); omit to replay from the start of retained history. */ + sinceSeq?: number; +} + +export interface AcceptResult { + docId: string; + role: string; + snapshot: Uint8Array | null; + atSeq: number; +} + +/** The seam. Implemented by the relay transport (phase 1) and, later, by an + MLS-wrapping transport and any fast-path/interop adapters. */ +export interface SyncTransport { + /** Attach to a document's channel as the current user. */ + join(docId: string, opts?: JoinOptions): Promise; + /** Create an invitation for an AT identity with a document-defined role. */ + invite(docId: string, inviteeDid: string, role: string): Promise; + /** Standing subscription to this user's invitation inbox. */ + inbox(): AsyncIterable; + /** Accept an invitation: returns the snapshot to load before syncing. */ + accept(token: string): Promise; + /** Fetch the latest snapshot blob for a document, if any. */ + fetchSnapshot(docId: string): Promise<{ bytes: Uint8Array; atSeq: number } | null>; + close(): void; +} + +// ——— Relay wire protocol (WebSocket, one connection per client) ——— +// +// Frames are JSON with binary payloads base64url-encoded in `b64`. (Phase 2 +// may switch to a binary framing; kept simple and inspectable for now.) + +export type ClientFrame = + | { t: 'join'; docId: string; sinceSeq?: number; manifest?: SyncManifest } + | { t: 'leave'; docId: string } + | { t: 'append'; docId: string; id: string; b64: string } + | { t: 'ephemeral'; docId: string; b64: string } + | { t: 'snapshot'; docId: string; atSeq: number; b64: string } + | { t: 'invite'; docId: string; inviteeDid: string; role: string; id: string; title?: string; payload?: string } + | { t: 'accept'; token: string; id: string } + | { t: 'sub-inbox' } + // KeyPackage directory (stands in for the PDS record until AT sign-in). + | { t: 'pub-kp'; b64: string } + | { t: 'get-kp'; did: string; id: string } + // Fetch an invitation by token (deep-link accepts that beat the inbox). + | { t: 'get-invite'; token: string; id: string }; + +export type ServerFrame = + | { t: 'joined'; docId: string; me: Member; roster: Member[] } + | { t: 'msg'; docId: string; seq: number; author: string; role: string; b64: string } + | { t: 'eph'; docId: string; author: string; role: string; b64: string } + | { t: 'roster'; docId: string; roster: Member[] } + | { t: 'ack'; id: string; seq?: number } + | { t: 'invited'; id: string; invitation: Invitation } + | { t: 'accepted'; id: string; result: { docId: string; role: string; atSeq: number; snapshotB64: string | null } } + | { t: 'inbox'; invitation: Invitation } + | { t: 'kp'; id: string; b64: string | null } + | { t: 'invite-info'; id: string; invitation: Invitation | null } + | { t: 'err'; id?: string; code: string; message: string }; + +export function toB64(bytes: Uint8Array): string { + let bin = ''; + for (const b of bytes) bin += String.fromCharCode(b); + return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +export function fromB64(s: string): Uint8Array { + const pad = s.replace(/-/g, '+').replace(/_/g, '/'); + const bin = atob(pad + '='.repeat((4 - (pad.length % 4)) % 4)); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; +} diff --git a/src/sync/relay-client.ts b/src/sync/relay-client.ts new file mode 100644 index 0000000..822ffe5 --- /dev/null +++ b/src/sync/relay-client.ts @@ -0,0 +1,283 @@ +// The base (plaintext) SyncTransport: one WebSocket to the relay, multiplexing +// every joined document plus the invitation inbox. Portable across Deno +// (runtime/tests) and the browser. Auth is a caller-supplied token provider +// (dev HMAC now; atproto service-auth later) — the transport never mints it. + +import { + type AcceptResult, + type ClientFrame, + type EphemeralMessage, + fromB64, + type Invitation, + type JoinOptions, + type LogMessage, + type Member, + type ServerFrame, + type Session, + type SyncManifest, + type SyncTransport, + toB64, +} from './protocol.ts'; + +interface Pending { + resolve: (frame: ServerFrame) => void; + reject: (err: Error) => void; +} + +class RelaySession implements Session { + me!: Member; + #roster: Member[] = []; + #msg = new Set<(m: LogMessage) => void>(); + #eph = new Set<(m: EphemeralMessage) => void>(); + #ros = new Set<(r: Member[]) => void>(); + + constructor(readonly docId: string, private conn: RelaySyncTransport) {} + + _init(me: Member, roster: Member[]) { + this.me = me; + this.#roster = roster; + } + _message(m: LogMessage) { + for (const h of this.#msg) h(m); + } + _ephemeral(m: EphemeralMessage) { + for (const h of this.#eph) h(m); + } + _roster(r: Member[]) { + this.#roster = r; + for (const h of this.#ros) h(r); + } + + roster() { + return this.#roster.slice(); + } + append(bytes: Uint8Array): Promise { + return this.conn._append(this.docId, bytes); + } + ephemeral(bytes: Uint8Array): void { + this.conn._send({ t: 'ephemeral', docId: this.docId, b64: toB64(bytes) }); + } + snapshot(bytes: Uint8Array, atSeq: number): Promise { + this.conn._send({ t: 'snapshot', docId: this.docId, atSeq, b64: toB64(bytes) }); + return Promise.resolve(); + } + onMessage(h: (m: LogMessage) => void) { + this.#msg.add(h); + return () => this.#msg.delete(h); + } + onEphemeral(h: (m: EphemeralMessage) => void) { + this.#eph.add(h); + return () => this.#eph.delete(h); + } + onRoster(h: (r: Member[]) => void) { + this.#ros.add(h); + return () => this.#ros.delete(h); + } + leave() { + this.conn._send({ t: 'leave', docId: this.docId }); + this.conn._dropSession(this.docId); + } +} + +export interface RelayClientOptions { + url: string; // ws://host/ws + did: string; + handle?: string; + /** Returns a fresh bearer token (dev HMAC or atproto service-auth JWT). */ + token: () => Promise | string; + /** For creating documents: the role manifest to register. */ + manifestFor?: (docId: string) => SyncManifest | undefined; +} + +export class RelaySyncTransport implements SyncTransport { + #ws?: WebSocket; + #ready?: Promise; + #seq = 0; + #pending = new Map(); + #sessions = new Map(); + #joinWaiters = new Map(); + #inboxHandlers = new Set<(inv: Invitation) => void>(); + #inboxBuffer: Invitation[] = []; + #subbedInbox = false; + + constructor(private opts: RelayClientOptions) {} + + async #connect(): Promise { + if (this.#ready) return this.#ready; + this.#ready = (async () => { + const token = await this.opts.token(); + const url = `${this.opts.url}?token=${encodeURIComponent(token)}` + + (this.opts.handle ? `&handle=${encodeURIComponent(this.opts.handle)}` : ''); + const ws = new WebSocket(url); + this.#ws = ws; + ws.addEventListener('message', (ev) => { + if (typeof ev.data === 'string') this.#onFrame(JSON.parse(ev.data)); + }); + await new Promise((res, rej) => { + ws.addEventListener('open', () => res()); + ws.addEventListener('error', () => rej(new Error('relay connection failed'))); + ws.addEventListener('close', () => { + for (const p of this.#pending.values()) p.reject(new Error('connection closed')); + this.#pending.clear(); + }); + }); + })(); + return this.#ready; + } + + #onFrame(f: ServerFrame) { + switch (f.t) { + case 'joined': { + const s = this.#sessions.get(f.docId); + if (s) s._init(f.me, f.roster); + this.#joinWaiters.get(f.docId)?.resolve(f); + this.#joinWaiters.delete(f.docId); + break; + } + case 'msg': + this.#sessions.get(f.docId)?._message({ seq: f.seq, author: f.author, role: f.role, bytes: fromB64(f.b64) }); + break; + case 'eph': + this.#sessions.get(f.docId)?._ephemeral({ author: f.author, role: f.role, bytes: fromB64(f.b64) }); + break; + case 'roster': + this.#sessions.get(f.docId)?._roster(f.roster); + break; + case 'ack': + case 'invited': + case 'accepted': + case 'kp': + case 'invite-info': + if (f.id) { + this.#pending.get(f.id)?.resolve(f); + this.#pending.delete(f.id); + } + break; + case 'inbox': + if (this.#inboxHandlers.size) for (const h of this.#inboxHandlers) h(f.invitation); + else this.#inboxBuffer.push(f.invitation); + break; + case 'err': + if (f.id) { + this.#pending.get(f.id)?.reject(new Error(`${f.code}: ${f.message}`)); + this.#pending.delete(f.id); + } + for (const w of this.#joinWaiters.values()) w.reject(new Error(`${f.code}: ${f.message}`)); + this.#joinWaiters.clear(); + break; + } + } + + _send(frame: ClientFrame) { + this.#ws?.send(JSON.stringify(frame)); + } + + #request(build: (id: string) => ClientFrame): Promise { + const id = `r${++this.#seq}`; + return new Promise((resolve, reject) => { + this.#pending.set(id, { resolve, reject }); + this._send(build(id)); + }); + } + + async _append(docId: string, bytes: Uint8Array): Promise { + const f = await this.#request((id) => ({ t: 'append', docId, id, b64: toB64(bytes) })); + return f.t === 'ack' ? (f.seq ?? 0) : 0; + } + + _dropSession(docId: string) { + this.#sessions.delete(docId); + } + + async join(docId: string, opts?: JoinOptions): Promise { + await this.#connect(); + const session = new RelaySession(docId, this); + this.#sessions.set(docId, session); + const manifest = this.opts.manifestFor?.(docId); + await new Promise((resolve, reject) => { + this.#joinWaiters.set(docId, { resolve: () => resolve(), reject }); + this._send({ t: 'join', docId, sinceSeq: opts?.sinceSeq, ...(manifest ? { manifest } : {}) } as ClientFrame); + }); + return session; + } + + async invite(docId: string, inviteeDid: string, role: string, extra?: { title?: string; payload?: string }): Promise { + await this.#connect(); + const f = await this.#request((id) => ({ t: 'invite', docId, inviteeDid, role, id, ...extra })); + if (f.t !== 'invited') throw new Error('invite failed'); + return f.invitation; + } + + async *inbox(): AsyncIterable { + await this.#connect(); + if (!this.#subbedInbox) { + this.#subbedInbox = true; + this._send({ t: 'sub-inbox' }); + } + const queue: Invitation[] = [...this.#inboxBuffer]; + this.#inboxBuffer = []; + let notify: (() => void) | undefined; + const handler = (inv: Invitation) => { + queue.push(inv); + notify?.(); + }; + this.#inboxHandlers.add(handler); + try { + while (true) { + if (queue.length) { + yield queue.shift()!; + continue; + } + await new Promise((r) => (notify = r)); + } + } finally { + this.#inboxHandlers.delete(handler); + } + } + + async accept(token: string): Promise { + await this.#connect(); + const f = await this.#request((id) => ({ t: 'accept', token, id })); + if (f.t !== 'accepted') throw new Error('accept failed'); + return { + docId: f.result.docId, + role: f.result.role, + snapshot: f.result.snapshotB64 ? fromB64(f.result.snapshotB64) : null, + atSeq: f.result.atSeq, + }; + } + + fetchSnapshot(): Promise<{ bytes: Uint8Array; atSeq: number } | null> { + // Snapshots arrive via accept() in this transport; direct fetch is unused. + return Promise.resolve(null); + } + + /** Publish this DID's MLS KeyPackage to the relay's directory. */ + async publishKeyPackage(bytes: Uint8Array): Promise { + await this.#connect(); + this._send({ t: 'pub-kp', b64: toB64(bytes) }); + } + + /** Fetch a member's published KeyPackage, if any. */ + async getKeyPackage(did: string): Promise { + await this.#connect(); + const f = await this.#request((id) => ({ t: 'get-kp', did, id })); + return f.t === 'kp' && f.b64 ? fromB64(f.b64) : null; + } + + /** Fetch an invitation by token (deep-link accepts; invitee-only). */ + async getInvite(token: string): Promise { + await this.#connect(); + const f = await this.#request((id) => ({ t: 'get-invite', token, id })); + return f.t === 'invite-info' ? f.invitation : null; + } + + /** Whether the relay connection is currently up. */ + get connected(): boolean { + return this.#ws?.readyState === WebSocket.OPEN; + } + + close() { + this.#ws?.close(); + } +} diff --git a/src/types.ts b/src/types.ts index c5c666c..8134400 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,6 +28,37 @@ export interface ModelEntry { hasIcon: boolean; } +export interface SyncMember { + did: string; + handle?: string; + role: string; +} + +export interface SyncFrame { + seq: number; + author: string; + role: string; + b64: string; +} + +export interface PendingInvitation { + token: string; + docId: string; + title: string; + inviterDid: string; + inviterHandle?: string; + role: string; + createdAt: number; +} + +export interface SyncState { + identity: { did: string; handle: string }; + at: { signedIn: boolean; handle?: string; did?: string }; + relayOk: boolean; + invitations: PendingInvitation[]; + docs: Record; +} + export interface ServerState { recents: RecentEntry[]; models: ModelEntry[]; @@ -36,6 +67,7 @@ export interface ServerState { mainBounds?: Bounds; tileServerPort: number; desktop: boolean; + sync?: SyncState; } export interface Bounds { diff --git a/src/ui/data-protocol.ts b/src/ui/data-protocol.ts index 0597f91..5415cee 100644 --- a/src/ui/data-protocol.ts +++ b/src/ui/data-protocol.ts @@ -2,7 +2,10 @@ // the shuttle forwards `tiles-protocol-up-data-*` messages from the tile up // to this window, and relays `tiles-protocol-down-data-payload` back down. // Ziran routes `ziranStore` payloads (the store.js loadData/putData contract) -// to the backend, which rewrites the tile file's self-storage. +// to the backend, which rewrites the tile file's self-storage — and +// `ziranSync` payloads (the sync.js channel) to the runtime's collaboration +// manager. The tile only ever sees opaque, attributed frames; keys and +// membership never cross this bridge. import { backend } from '../state/backend.ts'; @@ -17,6 +20,26 @@ interface StoreRequest { data?: Uint8Array; } +interface SyncRequest { + kind: 'hello' | 'send' | 'ephemeral'; + data?: Uint8Array | ArrayBuffer; +} + +function toB64(data: Uint8Array | ArrayBuffer | undefined): string { + const bytes = data instanceof Uint8Array ? data : new Uint8Array(data ?? new ArrayBuffer(0)); + let bin = ''; + for (const b of bytes) bin += String.fromCharCode(b); + return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +function fromB64(s: string): Uint8Array { + const pad = s.replace(/-/g, '+').replace(/_/g, '/'); + const bin = atob(pad + '='.repeat((4 - (pad.length % 4)) % 4)); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; +} + class DataProtocolHub { #frames = new Map(); @@ -29,7 +52,53 @@ class DataProtocolHub { if (action === UP_PAYLOAD && ev.data.payload?.ziranStore) { this.#handleStore(docId, ev.data.payload.ziranStore as StoreRequest); } + if (action === UP_PAYLOAD && ev.data.payload?.ziranSync) { + this.#handleSync(docId, ev.data.payload.ziranSync as SyncRequest); + } }); + // Incoming collaboration traffic: decrypted host-side, forwarded down to + // whichever registered tile the doc id belongs to. + const on = (type: string, fn: (d: Record) => void) => + backend.events.addEventListener(type, (ev) => fn((ev as CustomEvent).detail)); + on('sync-msg', (d) => + this.send(String(d.docId), { + ziranSync: { + kind: 'message', + seq: d.seq, + author: d.author, + role: d.role, + data: fromB64(String(d.b64)), + }, + })); + on('sync-eph', (d) => + this.send(String(d.docId), { + ziranSync: { kind: 'ephemeral', author: d.author, role: d.role, data: fromB64(String(d.b64)) }, + })); + on('sync-roster', (d) => + this.send(String(d.docId), { ziranSync: { kind: 'roster', roster: d.roster } })); + on('sync-ready', (d) => + this.send(String(d.docId), { ziranSync: { kind: 'ready', me: d.me, roster: d.roster } })); + } + + async #handleSync(docId: string, req: SyncRequest): Promise { + try { + if (req.kind === 'hello') { + const at = await backend.syncAttach(docId); + if (!at.shared) return; // solo doc: the tile simply never gets 'ready' + this.send(docId, { ziranSync: { kind: 'ready', me: at.me, roster: at.roster } }); + for (const m of at.buffered ?? []) { + this.send(docId, { + ziranSync: { kind: 'message', seq: m.seq, author: m.author, role: m.role, data: fromB64(m.b64) }, + }); + } + } else if (req.kind === 'send') { + await backend.syncSend(docId, toB64(req.data)); + } else if (req.kind === 'ephemeral') { + await backend.syncEphemeral(docId, toB64(req.data)); + } + } catch { + // Sync hiccups must never break the tile; the panel shows relay state. + } } async #handleStore(docId: string, req: StoreRequest): Promise { diff --git a/static/app.js b/static/app.js index cca86fc..9cf1fff 100644 --- a/static/app.js +++ b/static/app.js @@ -645,12 +645,19 @@ async function post(url, body) { } var backend = { onState: void 0, + /** Non-state events (sync traffic); fan-out to whoever subscribes. */ + events: new EventTarget(), connect(winId) { const ws = new WebSocket(keyed(`ws://${location.host}/api/events?win=${encodeURIComponent(winId)}`)); ws.addEventListener("message", (ev) => { try { const msg = JSON.parse(ev.data); if (msg.type === "state") this.onState?.(msg.state); + else if (typeof msg.type === "string") { + this.events.dispatchEvent(new CustomEvent(msg.type, { + detail: msg + })); + } } catch { } }); @@ -706,6 +713,25 @@ var backend = { if (!res.ok) throw new Error(data.error ?? "import failed"); return data; }, + /* ——— collaboration ——— */ + syncShare: (docId) => post(`/api/docs/${docId}/sync/share`), + syncInvite: (docId, did, role) => post(`/api/docs/${docId}/sync/invite`, { + did, + role + }), + syncAttach: (docId) => post(`/api/docs/${docId}/sync/attach`), + syncSend: (docId, b64) => post(`/api/docs/${docId}/sync/send`, { + b64 + }), + syncEphemeral: (docId, b64) => post(`/api/docs/${docId}/sync/ephemeral`, { + b64 + }), + acceptInvitation: (token) => post(`/api/invitations/${token}/accept`), + dismissInvitation: (token) => post(`/api/invitations/${token}/dismiss`), + atSignIn: (identifier) => post("/api/at/signin", { + identifier + }), + atSignOut: () => post("/api/at/signout"), loadData: async (docId, name) => { const res = await fetch(`/api/docs/${docId}/data/${name}`, { headers: authHeaders() @@ -754,6 +780,7 @@ var DeskStore = class extends EventTarget { tileServerPort = 0; desktop = false; connected = false; + sync; /** Which window this page renders. */ winId = "base"; sawMyWindow = false; @@ -770,6 +797,7 @@ var DeskStore = class extends EventTarget { this.windows = state.windows; this.tileServerPort = state.tileServerPort; this.desktop = state.desktop; + this.sync = state.sync; this.connected = true; this.docs = new Map(state.open.map((d3) => [ d3.id, @@ -1000,6 +1028,66 @@ var DeskStore = class extends EventTarget { this.toast(`Could not reveal: ${err.message}`); } } + /* ——— collaboration ——— */ + /** Live-session info for a doc, if it is shared. */ + syncDoc(docId) { + return this.sync?.docs?.[docId]; + } + async startSharing(docId) { + try { + await backend.syncShare(docId); + this.toast("This document is now live \u2014 invite people from this panel"); + } catch (err) { + this.toast(`Could not start sharing: ${err.message}`); + } + } + /** Returns the share link on success (also toasts it). */ + async invite(docId, did, role) { + try { + const { link } = await backend.syncInvite(docId, did, role); + try { + await navigator.clipboard.writeText(link); + this.toast("Invitation sent \u2014 share link copied to your clipboard"); + } catch { + this.toast(`Invitation sent \u2014 share link: ${link}`); + } + return link; + } catch (err) { + this.toast(`Could not invite: ${err.message}`); + return void 0; + } + } + async acceptInvitation(inv) { + try { + this.handleOpened(await backend.acceptInvitation(inv.token)); + this.toast(`\u201C${inv.title}\u201D is yours now \u2014 synced live with the others`); + } catch (err) { + this.toast(`Could not accept: ${err.message}`); + } + } + async dismissInvitation(inv) { + try { + await backend.dismissInvitation(inv.token); + } catch (err) { + this.toast(err.message); + } + } + async atSignIn(identifier) { + try { + await backend.atSignIn(identifier); + this.toast("Continue in your browser \u2014 Ziran opened your sign-in page"); + } catch (err) { + this.toast(`Could not start sign-in: ${err.message}`); + } + } + async atSignOut() { + try { + await backend.atSignOut(); + this.toast("Signed out \u2014 back to this machine\u2019s local identity"); + } catch (err) { + this.toast(err.message); + } + } toast(message) { this.dispatchEvent(new CustomEvent("toast", { detail: message @@ -1015,6 +1103,19 @@ var desk = new DeskStore(); var UP_PREFIX = "tiles-protocol-up-data-"; var UP_PAYLOAD = `${UP_PREFIX}payload`; var DOWN_PAYLOAD = "tiles-protocol-down-data-payload"; +function toB64(data) { + const bytes = data instanceof Uint8Array ? data : new Uint8Array(data ?? new ArrayBuffer(0)); + let bin = ""; + for (const b3 of bytes) bin += String.fromCharCode(b3); + return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} +function fromB64(s4) { + const pad = s4.replace(/-/g, "+").replace(/_/g, "/"); + const bin = atob(pad + "=".repeat((4 - pad.length % 4) % 4)); + const out = new Uint8Array(bin.length); + for (let i5 = 0; i5 < bin.length; i5++) out[i5] = bin.charCodeAt(i5); + return out; +} var DataProtocolHub = class { #frames = /* @__PURE__ */ new Map(); init() { @@ -1026,7 +1127,72 @@ var DataProtocolHub = class { if (action === UP_PAYLOAD && ev.data.payload?.ziranStore) { this.#handleStore(docId, ev.data.payload.ziranStore); } + if (action === UP_PAYLOAD && ev.data.payload?.ziranSync) { + this.#handleSync(docId, ev.data.payload.ziranSync); + } }); + const on = (type, fn) => backend.events.addEventListener(type, (ev) => fn(ev.detail)); + on("sync-msg", (d3) => this.send(String(d3.docId), { + ziranSync: { + kind: "message", + seq: d3.seq, + author: d3.author, + role: d3.role, + data: fromB64(String(d3.b64)) + } + })); + on("sync-eph", (d3) => this.send(String(d3.docId), { + ziranSync: { + kind: "ephemeral", + author: d3.author, + role: d3.role, + data: fromB64(String(d3.b64)) + } + })); + on("sync-roster", (d3) => this.send(String(d3.docId), { + ziranSync: { + kind: "roster", + roster: d3.roster + } + })); + on("sync-ready", (d3) => this.send(String(d3.docId), { + ziranSync: { + kind: "ready", + me: d3.me, + roster: d3.roster + } + })); + } + async #handleSync(docId, req) { + try { + if (req.kind === "hello") { + const at = await backend.syncAttach(docId); + if (!at.shared) return; + this.send(docId, { + ziranSync: { + kind: "ready", + me: at.me, + roster: at.roster + } + }); + for (const m2 of at.buffered ?? []) { + this.send(docId, { + ziranSync: { + kind: "message", + seq: m2.seq, + author: m2.author, + role: m2.role, + data: fromB64(m2.b64) + } + }); + } + } else if (req.kind === "send") { + await backend.syncSend(docId, toB64(req.data)); + } else if (req.kind === "ephemeral") { + await backend.syncEphemeral(docId, toB64(req.data)); + } + } catch { + } } async #handleStore(docId, req) { const reply = (msg) => this.send(docId, { @@ -1247,6 +1413,163 @@ var ZnBaseWindow = class extends i4 { outline: 2px solid var(--signal-strong); outline-offset: 2px; } + .id-signin { + margin-left: 0; + } + .id-chip { + display: inline-flex; + align-items: center; + gap: 7px; + font: inherit; + font-size: var(--text-sm); + font-weight: var(--w-medium); + color: var(--on-ink); + background: oklch(0.3 0.01 352); + border: 1px solid transparent; + border-radius: var(--r-full); + padding: 5px 14px; + cursor: pointer; + white-space: nowrap; + transition: background var(--t-fast) var(--ease-out); + } + .id-chip:hover { + background: oklch(0.35 0.012 352); + } + .id-chip:focus-visible { + outline: 2px solid var(--signal-strong); + outline-offset: 2px; + } + .id-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--signal); + } + .id-pop { + top: 60px; + right: var(--sp-5); + left: auto; + max-width: 320px; + } + .id-did { + margin: var(--sp-1) var(--sp-2); + font-family: var(--font-mono); + font-size: 0.72rem; + color: var(--ink-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .signin-form { + display: grid; + gap: var(--sp-2); + padding: var(--sp-2); + min-width: 260px; + } + .signin-form label { + font-size: var(--text-sm); + font-weight: var(--w-semibold); + color: var(--ink); + } + .signin-form input { + font: inherit; + font-size: var(--text-sm); + padding: 6px 12px; + border: 1px solid var(--line); + border-radius: var(--r-sm); + color: var(--ink); + background: var(--surface); + } + .signin-form input:focus-visible { + outline: 2px solid var(--signal); + outline-offset: 1px; + } + .menu button.signin-go { + font: inherit; + font-size: var(--text-sm); + font-weight: var(--w-medium); + border: 1px solid var(--ink); + background: var(--ink); + color: var(--surface); + border-radius: var(--r-full); + padding: 5px 14px; + cursor: pointer; + justify-self: start; + } + .menu button.signin-go:focus-visible { + outline: 2px solid var(--signal); + outline-offset: 2px; + } + .signin-fine { + margin: 0; + font-size: var(--text-xs); + color: var(--ink-muted); + line-height: 1.45; + text-wrap: pretty; + } + .invites { + display: grid; + gap: var(--sp-1); + padding: var(--sp-2) var(--sp-6); + border-bottom: 1px solid var(--line-soft); + background: var(--surface); + } + .invite-row { + display: flex; + align-items: center; + gap: var(--sp-2); + min-height: 32px; + min-width: 0; + } + .invite-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--signal); + flex: none; + } + .invite-text { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: var(--text-sm); + color: var(--ink); + } + .invite-accept, + .invite-dismiss { + font: inherit; + font-size: var(--text-sm); + font-weight: var(--w-medium); + border-radius: var(--r-full); + padding: 3px 12px; + cursor: pointer; + white-space: nowrap; + transition: border-color var(--t-fast) var(--ease-out), background var(--t-fast) var(--ease-out); + } + .invite-accept { + border: 1px solid var(--ink); + background: var(--ink); + color: var(--surface); + } + .invite-accept:hover { + background: var(--ink-strong, var(--ink)); + border-color: var(--ink-strong, var(--ink)); + } + .invite-dismiss { + border: 1px solid var(--line); + background: var(--surface); + color: var(--ink-muted); + } + .invite-dismiss:hover { + border-color: var(--ink-muted); + } + .invite-accept:focus-visible, + .invite-dismiss:focus-visible { + outline: 2px solid var(--signal); + outline-offset: 2px; + } .regions { flex: 1; display: grid; @@ -1538,7 +1861,9 @@ var ZnBaseWindow = class extends i4 { ⌘K + ${this.renderIdentity()} + ${this.renderInvitations()}

Recent

@@ -1576,6 +1901,73 @@ var ZnBaseWindow = class extends i4 { `; } + /** The identity affordance: your AT handle when signed in, a quiet way in + when not. Collaboration works either way — signing in makes you + reachable by handle across machines. */ + renderIdentity() { + const at = desk.sync?.at; + if (at?.signedIn) { + return b2` + + + `; + } + return b2` + + + `; + } + idAction(fn) { + this.renderRoot.querySelector(".id-pop")?.hidePopover(); + fn(); + } + /** Pending invitations: a strip between banner and regions, present only + when someone is actually waiting on you. */ + renderInvitations() { + const invitations = desk.sync?.invitations ?? []; + if (!invitations.length) return A; + return b2` +
+ ${invitations.map((inv) => b2` +
+ + + ${inv.inviterHandle ?? inv.inviterDid} invited you to + “${inv.title}” as ${inv.role} + + + +
+ `)} +
+ `; + } tileIcon(url) { return url ? b2`` : b2`${icons.tile}`; } @@ -1774,6 +2166,19 @@ var ZnConnections = class extends i4 { color: var(--ink-muted); transform: scale(0.9); } + .chip-live { + display: inline-grid; + place-items: center; + min-width: 16px; + height: 16px; + padding: 0 4px; + border-radius: var(--r-full); + background: var(--signal); + color: #fff; + font-size: 0.6875rem; + font-weight: var(--w-semibold); + line-height: 1; + } .mark { display: inline-grid; transform: scale(0.8); @@ -1873,12 +2278,72 @@ var ZnConnections = class extends i4 { outline: 2px solid var(--signal); outline-offset: 2px; } + ul.members { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 2px; + } + ul.members li { + display: flex; + align-items: center; + gap: var(--sp-2); + min-height: 24px; + min-width: 0; + } + .member-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .presence { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--signal); + flex: none; + } + form.invite { + display: flex; + gap: var(--sp-2); + min-width: 0; + } + form.invite input { + flex: 1; + min-width: 0; + font: inherit; + font-size: var(--text-sm); + padding: 3px 10px; + border: 1px solid var(--line); + border-radius: var(--r-full); + background: var(--surface); + color: var(--ink); + } + form.invite input::placeholder { + color: var(--ink-muted); + } + form.invite input:focus-visible, + form.invite select:focus-visible { + outline: 2px solid var(--signal); + outline-offset: 1px; + } + form.invite select { + font: inherit; + font-size: var(--text-sm); + border: 1px solid var(--line); + border-radius: var(--r-full); + background: var(--surface); + color: var(--ink); + padding: 3px 6px; + } ` ]; render() { const d3 = this.doc; const iconUrl = docIconUrl(d3); - const label = `Tile details for ${d3.name}: ${d3.damaged ? "failed verification" : "verified"}` + (d3.modelName ? `, made from the ${d3.modelName} model` : ""); + const live = desk.syncDoc(d3.id); + const label = `Tile details for ${d3.name}: ${d3.damaged ? "failed verification" : "verified"}` + (d3.modelName ? `, made from the ${d3.modelName} model` : "") + (live ? `, live with ${live.members.length} ${live.members.length === 1 ? "member" : "members"}` : ""); return b2` `; } + renderSyncSection() { + const sync = desk.sync; + if (!sync?.relayOk) { + return b2`

+ The relay is unreachable, so live sessions are paused. This document keeps working on + this machine. +

`; + } + const s4 = desk.syncDoc(this.doc.id); + if (!s4) { + return b2` +
+ Only on this machine + +
+

+ Going live keeps the document here and end-to-end encrypted — the relay only ever + carries ciphertext. You choose who joins, and as what. +

+ `; + } + const roles = [ + "editor", + "watcher" + ]; + return b2` +
    + ${s4.members.map((m2) => b2` +
  • + + ${m2.handle ?? m2.did} + ${m2.did === sync.identity.did ? `${m2.role} \xB7 you` : m2.role} +
  • + `)} +
+
+ + + +
+

+ They get the invitation inside Ziran and as a link you can pass along. Accepting + downloads the document to their machine first, then joins them live. +

+ `; + } + async submitInvite(e4) { + e4.preventDefault(); + const form = e4.target; + const did = form.elements.namedItem("did").value.trim(); + const role = form.elements.namedItem("role").value; + if (!did) return; + const link = await desk.invite(this.doc.id, did, role); + if (link) form.elements.namedItem("did").value = ""; + } renderModelSection() { const d3 = this.doc; const saved = d3.modelId ? desk.models.find((m2) => m2.id === d3.modelId) : void 0; @@ -3437,6 +3963,30 @@ var ZnDocWindow = class extends i4 { place-items: center; color: var(--ink-muted); } + .bar .live { + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: var(--sp-2); + font-size: var(--text-sm); + color: var(--ink-muted); + white-space: nowrap; + } + .bar .live-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--signal); + } + @media (prefers-reduced-motion: no-preference) { + .bar .live-dot { + animation: live-pulse 2.4s var(--ease-out) infinite; + } + @keyframes live-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.45; } + } + } .bar .title { font-weight: var(--w-semibold); font-size: var(--text-ui); @@ -3688,6 +4238,18 @@ var ZnDocWindow = class extends i4 { focusPane(rec) { return rec.panes.find((p3) => p3.id === this.focusPaneId) ?? rec.panes[0]; } + /** The banner space reserved for sync info: present only when the document + is in a live session. The dot is signal pink — semantic (live), not + decoration. */ + renderLive(docId) { + const s4 = desk.syncDoc(docId); + if (!s4) return A; + const n4 = s4.members.length; + return b2` + + ${n4 === 1 ? "live \xB7 only you" : `live \xB7 ${n4} people`} + `; + } tileIcon(docId, size = 18) { const doc = desk.getDoc(docId); const url = doc ? docIconUrl(doc) : void 0; @@ -3719,6 +4281,7 @@ var ZnDocWindow = class extends i4 { @dblclick=${() => this.beginRename()} >${activeDoc.name}`} + ${this.renderLive(activeDoc.id)} ` : A} ${activeDoc ? b2`` : A} diff --git a/tiles/built/checklist.tile b/tiles/built/checklist.tile index 4943262b3fc66f9d89da1a7556f174f9547d9ebf..c7170145d2dba69101c42f7fbe954e87ca919171 100644 GIT binary patch delta 1965 zcmeyOy2oUL45PyIv(4q!$11G#17?>R=>|tEwNx;Y*{q%{ko|Dk*)m_|$u^7|)Hdld zh6)i>x><;64_m#yzJh;2v2K1@YLRY!QA%o2YKm@7e!4<(eojteQhrflNoIbYl|pJ+ zYEh*^az##egT()f`Wp+zCw0tWon8-Ql&y>NouY_ zW{RFdaDGm{jzWGxu|i@&K~AMYN@h`Na!F35o`O$iS!z8*Dm`DJB%>(5G(7`mP;q5m zvR-j&UW$gMLSkMDRC9iwLPU%@%ZB}5@FH8mwQ1uT=Okerd4oSl^FGgsy%PZr~R6$bKdPHKrlPG(uEf~`VYVoq_YH5b^f-^7v(y`sdtl>A%`O}&!*;F6-uymSp?GflnXoXq4@4I>=|Lj%pp z|2XG?V#FskJu$gbAvLe0C^NNKp`a)=C9xz`Av2}eS|K+xuLKkbxeCSk;9yNo%u`5A zNhwM#E(V8su^!kN3ShrzKwJZkjr=r-yrx2Cnu3NRNK7v?MH4KjmkDw#$k8k*8k*LV z<+&9m^YL&_-p5s|2KJ*p*sDH?NvSy+nhM$q>I#RpD-)IFTX&+*49=*JuxLk9U-fLh>Bvp z;{4oH4UJ52g2@C&h^?(cegP0f#Uo z?m;dBl@5@yDlfHs@*Zy0`uqY-O-;Ci!OGz&LIILYK+c4zP*5mJ%}W8LeS}DHVp*yN z!VF|jgYC=D3rbB)snmcJFtFqX@~eU^n?gxZX{z;Pd2Sg*&Vw0N3pEsG3MAJ-jR7Yc zkRh7j1YTOK2TE7Y`FY@S-WItKuvS1b$v3sQI59m{L!(+Dsj?)sSfN%^!B)%;?(veM zN`-2u=O+hpX^UxEE7U3^gJZ2)0g}zF6>2Ac5zv`DfyZ_84IUFlwvyCbz08!!JbZ@c zDbOg^s8+}?uu@P5C21XSUI3*cz04FH1yCZiQb@|L&;u9YsVNGznv=8nF0sXw8W^OQ zPqydJQ%5%jnkrFD0%^O(e@_=(EhzUw6oQJ%6b(&S1}sa=DV=;nKykCU;9W)l$Ul_Klv9A@8mgrO_N^;+c4`W zWNuawF=3n>Cu*oyt&mxgnyZ(RpO>m&tB{mmp_iPInw*`QVy#e=nwOGVq@ii8P@Gtn z3ZiQ#9}~UAba?vYd185*5S_((#RWN;$*CHdItqrG*fkl8-%|nEQmj`{TAZO#tx%F$ lQDUV~mY7qTs-pn1+DaiUF{gO4orL1#9m49HGbFAs0swU$bjJVy diff --git a/tiles/built/livetext.tile b/tiles/built/livetext.tile new file mode 100644 index 0000000000000000000000000000000000000000..a4f621c88cb10fcfa33f2abb36e58b61be6b87b8 GIT binary patch literal 103068 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�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 z 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

mb8dsi5eEjM+iT#e7>(zZgEM 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 PdL1XQxuD6jT5B!%>DWfmpo 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

+ + diff --git a/tiles/livetext/manifest.json b/tiles/livetext/manifest.json new file mode 100644 index 0000000..7a982fc --- /dev/null +++ b/tiles/livetext/manifest.json @@ -0,0 +1,20 @@ +{ + "name": "Untitled text", + "description": "Plain text, written together — everyone's cursor in view.", + "icons": [{ "src": "/icon.svg", "sizes": "any" }], + "sizing": { "width": 640, "height": 560 }, + "model": { + "id": "com.berjon.ziran.livetext", + "name": "Live text", + "description": "Plain text, written together — everyone's cursor in view.", + "icons": [{ "src": "/icon.svg", "sizes": "any" }] + }, + "sync": { + "owner": "author", + "roles": { + "author": { "write": true, "admin": true }, + "editor": { "write": true }, + "reader": { "write": false } + } + } +} diff --git a/tiles/livetext/vendor/yjs.js b/tiles/livetext/vendor/yjs.js new file mode 100644 index 0000000..9d6e2df --- /dev/null +++ b/tiles/livetext/vendor/yjs.js @@ -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},Hr=(e,t)=>{let n=[];for(let[r,s]of e)n.push(t(s,r));return n},Jr=(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 Xr=(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,xl=Number.isNaN;var Je=e=>e!==0?e<0:1/e<0;var Tt=Number.MAX_SAFE_INTEGER,Nn=Number.MIN_SAFE_INTEGER,bl=1<<31;var Kr=Number.isInteger||(e=>typeof e=="number"&&isFinite(e)&&B(e)===e),kl=Number.isNaN,Sl=Number.parseInt;var Bn=String.fromCharCode,$i=String.fromCodePoint,_l=Bn(65535),Fi=e=>e.toLowerCase(),ji=/^\s*/g,qi=e=>e.replace(ji,""),Gi=/([A-Z])/g,Mn=(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),Yr=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)=>Pr(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)},Rn=new Uint8Array(3e4),Pi=Rn.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)},$n=(e,t)=>{Xi(e,t);let n=new DataView(e.cbuf.buffer,e.cpos,t);return e.cpos+=t,n},Wi=(e,t)=>$n(e,4).setFloat32(0,t,!1),Zi=(e,t)=>$n(e,8).setFloat64(0,t,!1),Qi=(e,t)=>$n(e,8).setBigInt64(0,t,!1);var Zr=new DataView(new ArrayBuffer(4)),to=e=>(Zr.setFloat32(0,e),Zr.getFloat32(0)===e),Nt=(e,t)=>{switch(typeof t){case"string":D(e,119),lt(e,t);break;case"number":Kr(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 Qr=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++:(Qr(this),this.count=1,this.s=t)}toUint8Array(){return Qr(this),q(this.encoder)}};var ts=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++):(ts(this),this.count=1,this.diff=t-this.s,this.s=t)}toUint8Array(){return ts(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 ns=G("Unexpected end of array"),rs=G("Integer out of Range"),Rt=class{constructor(t){this.arr=t,this.pos=0}},v=e=>new Rt(e),jn=e=>e.pos!==e.arr.length;var no=(e,t)=>{let n=new Uint8Array(e.arr.buffer,e.pos+e.arr.byteOffset,t);return e.pos+=t,n},F=e=>no(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 ns},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 ns};var ro=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))}},so=e=>vt.decode(F(e)),ht=vt?so:ro;var qn=(e,t)=>{let n=new DataView(e.arr.buffer,e.arr.byteOffset+e.pos,t);return e.pos+=t,n},io=e=>qn(e,4).getFloat32(0,!1),oo=e=>qn(e,8).getFloat64(0,!1),co=e=>qn(e,8).getBigInt64(0,!1);var lo=[e=>{},e=>null,we,io,oo,co,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;rlo[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),jn(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 Cl=crypto.subtle,ss=crypto.getRandomValues.bind(crypto);var Gn=()=>ss(new Uint32Array(1))[0];var ho="10000000-1000-4000-8000"+-1e11,is=()=>ho.replace(/[018]/g,e=>(e^Gn()&15>>e/4).toString(16));var Ye=Date.now;var zn=e=>new Promise(e);var Dl=Promise.all.bind(Promise);var Hn=e=>e===void 0?null:e;var Jn=class{constructor(){this.map=new Map}setItem(t,n){this.map.set(t,n)}getItem(t){return this.map.get(t)}},cs=new Jn,go=!0;try{typeof localStorage<"u"&&localStorage&&(cs=localStorage,go=!1)}catch{}var ls=cs;var Et=Symbol("Equality"),We=(e,t)=>e===t||!!e?.[Et]?.(t)||!1;var as=e=>typeof e=="object",hs=Object.assign,wo=Object.keys;var us=(e,t)=>{for(let n in e)t(e[n],n)};var ye=e=>wo(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),Pn=(e,t)=>e===t||ye(e)===ye(t)&&jt(e,(n,r)=>(n!==void 0||xe(t,r))&&We(t[r],n)),yo=Object.freeze,Kn=e=>{for(let t in e){let n=e[t];(typeof n=="object"||typeof n=="function")&&Kn(e[t])}return yo(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 Tl=typeof navigator<"u"?/Mac/.test(navigator.platform):!1,W,xo=[],bo=()=>{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(`--${Mn(t,"-")}`,n),W.set(`-${Mn(t,"-")}`,n)}})):W=A();return W},Qn=e=>bo().has(e);var _e=e=>Se?Hn(process.env[e.toUpperCase().replaceAll("-","_")]):Hn(ls.getItem(e));var ps=e=>Qn("--"+e)||_e(e)!==null,gs=ps("production"),ko=Se&&fs(process.env.FORCE_COLOR,["true","1","2"]),ms=ko||!Qn("--no-colors")&&!ps("no-color")&&(!Se||process.stdout.isTTY)&&(!Se||Qn("--color")||_e("COLORTERM")!==null||(_e("TERM")||"").includes("color"));var So=e=>new Uint8Array(e);var ws=e=>{let t=So(e.byteLength);return t.set(e),t};var er=class{constructor(t,n){this.left=t,this.right=n}},Z=(e,t)=>new er(e,t);var nr=e=>e.next()>=.5,Ze=(e,t,n)=>B(e.next()*(n+1-t)+t);var rr=(e,t,n)=>B(e.next()*(n+1-t)+t);var sr=(e,t,n)=>rr(e,t,n);var Uo=e=>Bn(sr(e,97,122)),ys=(e,t=0,n=20)=>{let r=sr(e,t,n),s="";for(let i=0;it[sr(e,0,t.length-1)];var Ao=Symbol("0schema"),ir=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(` +`)}},or=(e,t)=>e===t?!0:e==null||t==null||e.constructor!==t.constructor?!1:e[Et]?We(e,t):st(e)?ze(e,n=>he(t,r=>or(n,r))):as(e)?jt(e,(n,r)=>or(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]),or(n,r)}equals(t){return this.constructor===t.constructor&&be(this.shape,t.shape)}[Ao](){return!0}[Et](t){return this.equals(t)}validate(t){return this.check(t)}check(t,n){z()}get nullable(){return Jt(this,dn)}get optional(){return new tn(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),Ol=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),Vl=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}},hn=(...e)=>new zt(e),bs=E(zt),Io=RegExp.escape||(e=>e.replace(/[().|&,$^[\]]/g,t=>"\\"+t)),ks=e=>{if(Ht.check(e))return[Io(e)];if(bs.check(e))return e.shape.map(t=>t+"");if(Cs.check(e))return["[+-]?\\d+.?\\d*"];if(As.check(e))return[".*"];if(ln.check(e))return e.shape.map(ks).flat(1);T()},cr=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 Nl=E(cr),Do=Symbol("optional"),tn=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[Do](){return!0}},To=E(tn),en=class extends O{check(t,n){return n?.extend(null,"never",typeof t),!1}},Ll=new en,Bl=E(en),nn=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 nn(e),Oo=E(nn),Vo=I(e=>e!=null&&(e.constructor===Object||e.constructor==null)),rn=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 rn(e,t),No=E(rn),sn=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})}},Lo=(...e)=>new sn(e),Ml=E(sn),on=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 on(e),Bo=E(on),Mo=I(e=>st(e)),cn=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}},Ro=(e,t=null)=>new cn(e,t),Rl=E(cn),$o=Ro(O),lr=class extends O{constructor(t){super(),this.len=t.length-1,this.args=Lo(...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 Fo=E(lr),jo=I(e=>typeof e=="function"),ar=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 $l=E(ar,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=>ln.check(t))>=0?Jt(...e.map(t=>Ae(t)).map(t=>ln.check(t)?t.shape:[t]).flat(1)):e.length===1?e[0]:new Ce(e),ln=E(Ce),Es=()=>!0,an=I(Es),qo=E(Ue,e=>e.shape===Es),ur=I(e=>typeof e=="bigint"),Go=I(e=>e===ur),Us=I(e=>typeof e=="symbol"),Fl=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),un=I(e=>typeof e=="boolean"),zo=I(e=>e===un),Is=hn(void 0),jl=E(zt,e=>e.shape.length===1&&e.shape[0]===void 0),ql=hn(void 0);var dn=hn(null),Ho=E(zt,e=>e.shape.length===1&&e.shape[0]===null),Gl=E(Uint8Array),zl=E(Ee,e=>e.shape===Uint8Array),Jo=Jt(Gt,Ht,dn,Is,ur,un,Us),Hl=(()=>{let e=_s(an),t=Ss(Ht,an),n=Jt(Gt,Ht,dn,un,e,t);return e.shape=n,t.shape.values=n,n})(),Ae=e=>{if($o.check(e))return e;if(Vo.check(e)){let t={};for(let n in e)t[n]=Ae(e[n]);return vo(t)}else{if(Mo.check(e))return Jt(...e.map(Ae));if(Jo.check(e))return hn(e);if(jo.check(e))return E(e)}T()},xs=gs?()=>{}:(e,t)=>{let n=new ir;if(!t.check(e,n))throw G(`Expected value to be of type ${t.constructor.name}. +${n.toString()}`)},hr=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(an,t)}done(){return(t,n)=>{for(let r=0;rnew hr(e),Ds=Xo(an).if(Cs,(e,t)=>Ze(t,Nn,Tt)).if(As,(e,t)=>ys(t)).if(zo,(e,t)=>nr(t)).if(Go,(e,t)=>BigInt(Ze(t,Nn,Tt))).if(ln,(e,t)=>qt(t,Qe(t,e.shape))).if(Oo,(e,t)=>{let n={};for(let r in e.shape){let s=e.shape[r];if(To.check(s)){if(nr(t))continue;s=s.shape}n[r]=Ds(s,t)}return n}).if(Bo,(e,t)=>{let n=[],r=rr(t,0,42);for(let s=0;sQe(t,e.shape)).if(Ho,(e,t)=>null).if(Fo,(e,t)=>{let n=qt(t,e.res);return()=>n}).if(qo,(e,t)=>qt(t,Qe(t,[Gt,Ht,dn,Is,ur,un,_s(Gt),Ss(Jt("a","b","c"),Gt)]))).if(No,(e,t)=>{let n={},r=Ze(t,0,3);for(let s=0;sDs(Ae(t),e);var Ut=typeof document<"u"?document:{};var Jl=I(e=>e.nodeType===Zo);var Xl=typeof DOMParser<"u"?new DOMParser:null;var Pl=I(e=>e.nodeType===Ko);var Kl=I(e=>e.nodeType===Yo);var Ts=e=>Hr(e,(t,n)=>`${n}:${t};`).join("");var Ko=Ut.ELEMENT_NODE,Yo=Ut.TEXT_NODE,Yl=Ut.CDATA_SECTION_NODE,Wl=Ut.COMMENT_NODE,Wo=Ut.DOCUMENT_NODE,Zl=Ut.DOCUMENT_TYPE_NODE,Zo=Ut.DOCUMENT_FRAGMENT_NODE,Ql=I(e=>e.nodeType===Wo);var Q=Symbol;var Ie=Q(),De=Q(),dr=Q(),fr=Q(),pr=Q(),Te=Q(),gr=Q(),Xt=Q(),mr=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))},wr=(...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}),pn=(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&&lc(n,t.clock)!==null},Vr=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))},Nr=()=>new gt,ri=e=>{let t=Nr();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},ac=(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 _n,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=zn(h=>{this.on("load",()=>{this.isLoaded=!0,h(this)})});let l=()=>zn(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)}},bn=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 bn{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])=>{hc(e,t.clients.get(s),s,i)})},uc=(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 _n,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 Lr(f,c,new Map),g(f.restEncoder,0),{missing:l,update:f.toUint8Array()}}return null},fc=(e,t)=>Lr(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=uc(r,o),h=dc(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=En([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=En([u,f]):c.pendingDs=u||f}else c.pendingDs=u;if(i){let d=c.pendingStructs.update;c.pendingStructs=null,Br(s.doc,d)}},n,!1),ha=(e,t,n)=>ii(e,t,n,new H(e)),Br=(e,t,n,r=j)=>{let s=v(t);ii(s,e,n,new r(s))},ua=(e,t,n)=>Br(e,t,n,H),pc=(e,t,n=new Map)=>{Lr(e,t.store,n),tt(e,ri(t.store))},gc=(e,t=new Uint8Array([0]),n=new X)=>{let r=ci(t);pc(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 Tc(s.map((i,o)=>o===0?i:Nc(i)));if(n.constructor===X)return En(s)}return s[0]},da=(e,t)=>gc(e,t,new ct),oi=e=>{let t=new Map,n=w(e.restDecoder);for(let r=0;roi(new At(v(e))),Mr=(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),mc=(e,t)=>Mr(e,je(t.store)),wc=(e,t=new Qt)=>(e instanceof Map?Mr(t,e):mc(t,e),t.toUint8Array()),fa=e=>wc(e,new mt),xr=class{constructor(){this.l=[]}},Fs=()=>new xr,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()},kn=(e,t)=>{for(;t!==null;){if(t.parent===e)return!0;t=t.parent._item}return!1},pa=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)||Nr(),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}},ga=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},ma=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),br=class{constructor(t,n,r=0){this.type=t,this.index=n,this.assoc=r}},yc=(e,t,n=0)=>new br(e,t,n),gn=(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)},wa=(e,t,n=0)=>{let r=e._start;if(n<0){if(t===0)return gn(e,null,n);t--}for(;r!==null;){if(!r.deleted&&r.countable){if(r.length>t)return gn(e,m(r.id.client,r.id.clock+t),n);t-=r.length}if(r.right===null&&n<0)return gn(e,r.lastId,n);r=r.right}return gn(e,null,n)},xc=(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},ya=e=>{let t=kt();return xc(t,e),q(t)},bc=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=jn(e)?we(e):0;return new Ve(t,n,r,s)},xa=e=>bc(v(e)),kc=(e,t)=>{let n=Kt(e,t),r=t.clock-n.id.clock;return{item:n,diff:r}},ba=(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?vr(r,s):kc(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?vr(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 yc(l,h,e.assoc)},ka=(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,Sn=class{constructor(t,n){this.ds=t,this.sv=n}},Sa=(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),Mr(t,e.sv),t.toUint8Array()),_a=e=>Sc(e,new mt),_c=(e,t=new bn(v(e)))=>new Sn(ot(t),oi(t)),Ea=e=>_c(e,new At(v(e))),hi=(e,t)=>new Sn(e,t),Ua=hi(Nr(),new Map),Ca=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),kr=(e,t)=>{let n=L(e.meta,kr,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))},Aa=(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)}),Br(n,i.toUint8Array(),"snapshot"),n},Ec=(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)Ec(e,t,H),_n=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=Uc,Sr=(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[Sr(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,Dn(e,i,n.clock-i.id.clock+1)),i},Cc=(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=Sr(e,t,n),c;do c=t[o++],it.deleteSet.clients.size===0&&!Jr(t.afterState,(n,r)=>t.beforeState.get(r)!==n)?!1:(Vr(t.deleteSet),fc(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)},yn=(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+yn(s,l)}})},Da=(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&&Hc(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+yn(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&&yn(f,p)}if(!n.local&&n.afterState.get(r.clientID)!==n.beforeState.get(r.clientID)&&(fn(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 _r(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},Er=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||kn(s,r))&&qr(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}=vr(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||kn(p,f))&&u.push(f)}}),Wt(c,h.deletions,f=>{f instanceof b&&i.some(p=>p===c.doc||kn(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 Gr=l.beforeState.get(S)||0,zr=k-Gr;zr>0&&Oe(d,S,Gr,zr)});let f=Ye(),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 Er(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||kn(S,k))&&qr(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.destroy=this.destroy.bind(this),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)&&wr("[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),this.doc.off("destroy",this.destroy),super.destroy()}};function*Ac(e){let t=w(e.restDecoder);for(let n=0;nIc(e,H),Ic=(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);fn("Structs: ",n);let i=ot(r);fn("DeleteSet: ",i)},va=e=>Dc(e,H),Dc=(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=[]}},Tc=e=>En(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),es(a,r.restEncoder),r.restEncoder=a,r.toUint8Array()}else return g(r.restEncoder,0),r.toUint8Array()},Oa=e=>vc(e,mt,H),Oc=(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}},Va=e=>Oc(e,H),Vc=(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))}},En=(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=Vc(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),Rr(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()}Rr(o);let h=ot(c);return tt(i,h),i.toUint8Array()},Na=(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++},Rr=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);Rr(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 An&&(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 In:{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()}}},La=(e,t)=>Tn(e,yi(t),H,ct),Ba=(e,t)=>Tn(e,yi(t),j,X),Ma=e=>Tn(e,Wn,H,X),Nc=e=>Tn(e,Wn,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=Lc(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}},Lc=(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=()=>{wr("Invalid access: Add Yjs type to a document before reading data.")},xi=80,$r=0,Ur=class{constructor(t,n){t.marker=!0,this.p=t,this.index=n,this.timestamp=$r++}},Bc=e=>{e.timestamp=$r++},bi=(e,t,n)=>{e.p.marker=!1,e.p=t,t.marker=!0,e.index=n,e.timestamp=$r++},Mc=(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))}},Ra=e=>{e.doc??V();let t=e._start,n=[];for(;t;)n.push(t),t=t.right;return n},On=(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},Rc=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=vn(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),Un(e,t,null,r);let s=n,i=vn(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 Un(e,t,s,n)},Ai=(e,t,n,r)=>{if(r===0)return;let s=n,i=r,o=vn(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)},Cn=(e,t,n)=>{let r=t._map.get(n);r!==void 0&&r.delete(e)},Fr=(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)},jr=(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},Fa=(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},Fc=(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},mn=e=>(e.doc??V(),Ls(e._map.entries(),t=>!t[1].deleted)),Cr=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),On(this,t,new Cr(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=>{$c(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 Rc(this)}_write(t){t.writeTypeRef(ll)}},jc=e=>new ee,Ar=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){On(this,t,new Ar(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[...mn(this)].length}keys(){return pn(mn(this),t=>t[0])}values(){return pn(mn(this),t=>t[1].content.getContent()[t[1].length-1])}entries(){return pn(mn(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=>{Cn(n,this,t)}):this._prelimContent.delete(t)}set(t,n){return this.doc!==null?x(this.doc,r=>{Fr(r,this,t,n)}):this._prelimContent.set(t,n),n}get(t){return jr(this,t)}has(t){return Di(this,t)}clear(){this.doc!==null?x(this.doc,t=>{this.forEach(function(n,r,s){Cn(t,s,r)})}):this._prelimContent.clear()}_write(t){t.writeTypeRef(al)}},qc=e=>new ne,ft=(e,t)=>e===t||typeof e=="object"&&typeof t=="object"&&e&&t&&Pn(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?vn(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},yr=(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},Gc=(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}},zc=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},Hc=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):Gc(r,s)});for(let s of t)zc(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 Ir(this,t,n);On(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)&&yr(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&&kr(u,t),n&&kr(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=wn(i,this,t,!r);r||(r={},o.currentAttributes.forEach((c,l)=>{r[l]=c})),yr(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=wn(i,this,t,!r);yr(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,wn(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=wn(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=>{Cn(n,this,t)}):this._pending.push(()=>this.removeAttribute(t))}setAttribute(t,n){this.doc!==null?x(this.doc,r=>{Fr(r,this,t,n)}):this._pending.push(()=>this.setAttribute(t,n))}getAttribute(t){return jr(this,t)}getAttributes(){return Ii(this)}_write(t){t.writeTypeRef(hl)}},Jc=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){On(this,t,new Dr(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;Un(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(dl)}},Xc=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()}`}removeAttribute(t){this.doc!==null?x(this.doc,n=>{Cn(n,this,t)}):this._prelimAttrs.delete(t)}setAttribute(t,n){this.doc!==null?x(this.doc,r=>{Fr(r,this,t,n)}):this._prelimAttrs.set(t,n)}getAttribute(t){return jr(this,t)}hasAttribute(t){return Di(this,t)}getAttributes(t){return t?Fc(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(ul),t.writeKey(this.nodeName)}},Pc=e=>new se(e.readKey()),Dr=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)})}},An=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(fl),t.writeKey(this.hookName)}},Kc=e=>new An(e.readKey()),Tr=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=0;s--)r+=``;return r}).join("")}toJSON(){return this.toString()}_write(t){t.writeTypeRef(pl)}},Yc=e=>new Tr,$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()}},Wc=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(Wc),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}},Zc=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}},Qc=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}},tl=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}},el=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}},nl=e=>new C(e.readKey(),e.readJSON()),In=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}},ol=e=>new nt(e.readString()),cl=[jc,qc,Jc,Pc,Xc,Kc,Yc],ll=0,al=1,hl=2,ul=3,dl=4,fl=5,pl=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}},gl=e=>new K(cl[e.readTypeRef()](e)),vr=(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}},qr=(e,t)=>{for(;e!==null&&e.keep!==t;)e.keep=t,e=e.parent._item},Dn=(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,qr(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?Cc(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)=>ml[t&31](e),ml=[()=>{T()},Qc,rl,Zc,ol,el,nl,gl,il,tl,()=>{T()}],wl=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(wl),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{br 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,In 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,Sn as Snapshot,Re as Text,_r as Transaction,Ws as UndoManager,H as UpdateDecoderV1,j as UpdateDecoderV2,ct as UpdateEncoderV1,X as UpdateEncoderV2,se as XmlElement,re as XmlFragment,An as XmlHook,Tr as XmlText,Cr as YArrayEvent,te as YEvent,Ar as YMapEvent,Ir as YTextEvent,Dr as YXmlEvent,ua as applyUpdate,Br as applyUpdateV2,zc as cleanupYTextFormatting,Pt as compareIDs,ka as compareRelativePositions,Ma as convertUpdateFormatV1ToV2,Nc as convertUpdateFormatV2ToV1,ba as createAbsolutePositionFromRelativePosition,Nr as createDeleteSet,ri as createDeleteSetFromStructStore,Aa as createDocFromSnapshot,m as createID,ma as createRelativePositionFromJSON,wa as createRelativePositionFromTypeIndex,hi as createSnapshot,xa as decodeRelativePosition,Ea as decodeSnapshot,_c as decodeSnapshotV2,ci as decodeStateVector,va as decodeUpdate,Dc as decodeUpdateV2,Na as diffUpdate,mi as diffUpdateV2,Ua as emptySnapshot,ya as encodeRelativePosition,_a as encodeSnapshot,Sc as encodeSnapshotV2,da as encodeStateAsUpdate,gc as encodeStateAsUpdateV2,fa as encodeStateVector,Oa as encodeStateVectorFromUpdate,vc as encodeStateVectorFromUpdateV2,ac as equalDeleteSets,Sa as equalSnapshots,P as findIndexSS,ai as findRootTypeKey,Kt as getItem,Js as getItemCleanEnd,M as getItemCleanStart,_ as getState,Ra as getTypeChildren,ce as isDeleted,kn as isParentOf,Wt as iterateDeletedStructs,pa as logType,Ta as logUpdate,Ic as logUpdateV2,Zt as mergeDeleteSets,Tc as mergeUpdates,En as mergeUpdatesV2,La as obfuscateUpdate,Ba as obfuscateUpdateV2,Va as parseUpdateMeta,Oc as parseUpdateMetaV2,ha as readUpdate,ii as readUpdateV2,ga as relativePositionToJSON,Ca as snapshot,Ia as snapshotContainsUpdate,x as transact,Da as tryGc,$a as typeListToArraySnapshot,Fc as typeMapGetAllSnapshot,Fa as typeMapGetSnapshot}; diff --git a/tiles/richtext/icon.svg b/tiles/richtext/icon.svg new file mode 100644 index 0000000..58b8a5d --- /dev/null +++ b/tiles/richtext/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tiles/richtext/index.html b/tiles/richtext/index.html new file mode 100644 index 0000000..d923379 --- /dev/null +++ b/tiles/richtext/index.html @@ -0,0 +1,748 @@ + + + + + + Rich text + + + +
+
+
+ +
+
+ + + + + diff --git a/tiles/richtext/manifest.json b/tiles/richtext/manifest.json new file mode 100644 index 0000000..87cbb1f --- /dev/null +++ b/tiles/richtext/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "Untitled document", + "description": "Rich text written together: comments, suggestions, images.", + "icons": [{ "src": "/icon.svg", "sizes": "any" }], + "sizing": { "width": 900, "height": 700 }, + "model": { + "id": "com.berjon.ziran.richtext", + "name": "Rich text", + "description": "Rich text written together: comments, suggestions, images.", + "icons": [{ "src": "/icon.svg", "sizes": "any" }] + }, + "sync": { + "owner": "author", + "roles": { + "author": { "write": true, "admin": true }, + "editor": { "write": true }, + "reviewer": { "write": true }, + "reader": { "write": false } + } + } +} diff --git a/tiles/richtext/vendor/pm.js b/tiles/richtext/vendor/pm.js new file mode 100644 index 0000000..4091293 --- /dev/null +++ b/tiles/richtext/vendor/pm.js @@ -0,0 +1,10 @@ +var Io=Object.defineProperty;var j=(r,e)=>{for(var t in e)Io(r,t,{get:e[t],enumerable:!0})};var wi={};j(wi,{AllSelection:()=>H,EditorState:()=>Rn,NodeSelection:()=>S,Plugin:()=>W,PluginKey:()=>Oe,Selection:()=>k,SelectionRange:()=>Fe,TextSelection:()=>C,Transaction:()=>Ht});var ei={};j(ei,{ContentMatch:()=>Se,DOMParser:()=>Xe,DOMSerializer:()=>ke,Fragment:()=>y,Mark:()=>O,MarkType:()=>Ye,Node:()=>Q,NodeRange:()=>be,NodeType:()=>dt,ReplaceError:()=>ce,ResolvedPos:()=>ut,Schema:()=>pt,Slice:()=>x});function v(r){this.content=r}v.prototype={constructor:v,find:function(r){for(var e=0;e>1}};v.from=function(r){if(r instanceof v)return r;var e=[];if(r)for(var t in r)e.push(t,r[t]);return new v(e)};var pn=v;function vr(r,e,t){for(let n=0;;n++){if(n==r.childCount||n==e.childCount)return r.childCount==e.childCount?null:t;let i=r.child(n),s=e.child(n);if(i==s){t+=i.nodeSize;continue}if(!i.sameMarkup(s))return t;if(i.isText&&i.text!=s.text){let o=i.text,l=s.text,a=0;for(;o[a]==l[a];a++)t++;return a&&a0&&u>0&&f[h-1]==c[u-1];)h--,u--,t--,n--;return h&&u&&h=56320&&r<57344}function Lr(r){return r>=55296&&r<56320}var y=class r{constructor(e,t){if(this.content=e,this.size=t||0,t==null)for(let n=0;ne&&n(a,i+l,s||null,o)!==!1&&a.content.size){let c=l+1;a.nodesBetween(Math.max(0,e-c),Math.min(a.content.size,t-c),n,i+c)}l=f}}descendants(e){this.nodesBetween(0,this.size,e)}textBetween(e,t,n,i){let s="",o=!0;return this.nodesBetween(e,t,(l,a)=>{let f=l.isText?l.text.slice(Math.max(e,a)-a,t-a):l.isLeaf?i?typeof i=="function"?i(l):i:l.type.spec.leafText?l.type.spec.leafText(l):"":"";l.isBlock&&(l.isLeaf&&f||l.isTextblock)&&n&&(o?o=!1:s+=n),s+=f},0),s}append(e){if(!e.size)return this;if(!this.size)return e;let t=this.lastChild,n=e.firstChild,i=this.content.slice(),s=0;for(t.isText&&t.sameMarkup(n)&&(i[i.length-1]=t.withText(t.text+n.text),s=1);se)for(let s=0,o=0;oe&&((ot)&&(l.isText?l=l.cut(Math.max(0,e-o),Math.min(l.text.length,t-o)):l=l.cut(Math.max(0,e-o-1),Math.min(l.content.size,t-o-1))),n.push(l),i+=l.nodeSize),o=a}return new r(n,i)}cutByIndex(e,t){return e==t?r.empty:e==0&&t==this.content.length?this:new r(this.content.slice(e,t))}replaceChild(e,t){let n=this.content[e];if(n==t)return this;let i=this.content.slice(),s=this.size+t.nodeSize-n.nodeSize;return i[e]=t,new r(i,s)}addToStart(e){return new r([e].concat(this.content),this.size+e.nodeSize)}addToEnd(e){return new r(this.content.concat(e),this.size+e.nodeSize)}eq(e){if(this.content.length!=e.content.length)return!1;for(let t=0;tthis.size||e<0)throw new RangeError(`Position ${e} outside of fragment (${this})`);for(let t=0,n=0;;t++){let i=this.child(t),s=n+i.nodeSize;if(s>=e)return s==e?zt(t+1,s):zt(t,n);n=s}}toString(){return"<"+this.toStringInner()+">"}toStringInner(){return this.content.join(", ")}toJSON(){return this.content.length?this.content.map(e=>e.toJSON()):null}static fromJSON(e,t){if(!t)return r.empty;if(!Array.isArray(t))throw new RangeError("Invalid input for Fragment.fromJSON");return r.fromArray(t.map(e.nodeFromJSON))}static fromArray(e){if(!e.length)return r.empty;let t,n=0;for(let i=0;ithis.type.rank&&(t||(t=e.slice(0,i)),t.push(this),n=!0),t&&t.push(s)}}return t||(t=e.slice()),n||t.push(this),t}removeFromSet(e){for(let t=0;tn.type.rank-i.type.rank),t}};O.none=[];var ce=class extends Error{},x=class r{constructor(e,t,n){this.content=e,this.openStart=t,this.openEnd=n}get size(){return this.content.size-this.openStart-this.openEnd}insertAt(e,t){let n=Jr(this.content,e+this.openStart,t,this.openStart+1,this.openEnd+1);return n&&new r(n,this.openStart,this.openEnd)}removeBetween(e,t){return new r(Wr(this.content,e+this.openStart,t+this.openStart),this.openStart,this.openEnd)}eq(e){return this.content.eq(e.content)&&this.openStart==e.openStart&&this.openEnd==e.openEnd}toString(){return this.content+"("+this.openStart+","+this.openEnd+")"}toJSON(){if(!this.content.size)return null;let e={content:this.content.toJSON()};return this.openStart>0&&(e.openStart=this.openStart),this.openEnd>0&&(e.openEnd=this.openEnd),e}static fromJSON(e,t){if(!t)return r.empty;let n=t.openStart||0,i=t.openEnd||0;if(typeof n!="number"||typeof i!="number")throw new RangeError("Invalid input for Slice.fromJSON");return new r(y.fromJSON(e,t.content),n,i)}static maxOpen(e,t=!0){let n=0,i=0;for(let s=e.firstChild;s&&!s.isLeaf&&(t||!s.type.spec.isolating);s=s.firstChild)n++;for(let s=e.lastChild;s&&!s.isLeaf&&(t||!s.type.spec.isolating);s=s.lastChild)i++;return new r(e,n,i)}};x.empty=new x(y.empty,0,0);function Wr(r,e,t){let{index:n,offset:i}=r.findIndex(e),s=r.maybeChild(n),{index:o,offset:l}=r.findIndex(t);if(i==e||s.isText){if(l!=t&&!r.child(o).isText)throw new RangeError("Removing non-flat range");return r.cut(0,e).append(r.cut(t))}if(n!=o)throw new RangeError("Removing non-flat range");return r.replaceChild(n,s.copy(Wr(s.content,e-i-1,t-i-1)))}function Jr(r,e,t,n,i,s){let{index:o,offset:l}=r.findIndex(e),a=r.maybeChild(o);if(l==e||a.isText)return s&&n<=0&&i<=0&&!s.canReplace(o,o,t)?null:r.cut(0,e).append(t).append(r.cut(e));let f=Jr(a.content,e-l-1,t,o==0?n-1:0,o==r.childCount-1?i-1:0,a);return f&&r.replaceChild(o,a.copy(f))}function Ro(r,e,t){if(t.openStart>r.depth)throw new ce("Inserted content deeper than insertion position");if(r.depth-t.openStart!=e.depth-t.openEnd)throw new ce("Inconsistent open depths");return $r(r,e,t,0)}function $r(r,e,t,n){let i=r.index(n),s=r.node(n);if(i==e.index(n)&&n=0&&r.isText&&r.sameMarkup(e[t])?e[t]=r.withText(e[t].text+r.text):e.push(r)}function ct(r,e,t,n){let i=(e||r).node(t),s=0,o=e?e.index(t):i.childCount;r&&(s=r.index(t),r.depth>t?s++:r.textOffset&&(Ie(r.nodeAfter,n),s++));for(let l=s;li&&gn(r,e,i+1),o=n.depth>i&&gn(t,n,i+1),l=[];return ct(null,r,i,l),s&&o&&e.index(i)==t.index(i)?(qr(s,o),Ie(Re(s,Kr(r,e,t,n,i+1)),l)):(s&&Ie(Re(s,Lt(r,e,i+1)),l),ct(e,t,i,l),o&&Ie(Re(o,Lt(t,n,i+1)),l)),ct(n,null,i,l),new y(l)}function Lt(r,e,t){let n=[];if(ct(null,r,t,n),r.depth>t){let i=gn(r,e,t+1);Ie(Re(i,Lt(r,e,t+1)),n)}return ct(e,null,t,n),new y(n)}function Po(r,e){let t=e.depth-r.openStart,i=e.node(t).copy(r.content);for(let s=t-1;s>=0;s--)i=e.node(s).copy(y.from(i));return{start:i.resolveNoCache(r.openStart+t),end:i.resolveNoCache(i.content.size-r.openEnd-t)}}var ut=class r{constructor(e,t,n){this.pos=e,this.path=t,this.parentOffset=n,this.depth=t.length/3-1}resolveDepth(e){return e==null?this.depth:e<0?this.depth+e:e}get parent(){return this.node(this.depth)}get doc(){return this.node(0)}node(e){return this.path[this.resolveDepth(e)*3]}index(e){return this.path[this.resolveDepth(e)*3+1]}indexAfter(e){return e=this.resolveDepth(e),this.index(e)+(e==this.depth&&!this.textOffset?0:1)}start(e){return e=this.resolveDepth(e),e==0?0:this.path[e*3-1]+1}end(e){return e=this.resolveDepth(e),this.start(e)+this.node(e).content.size}before(e){if(e=this.resolveDepth(e),!e)throw new RangeError("There is no position before the top-level node");return e==this.depth+1?this.pos:this.path[e*3-1]}after(e){if(e=this.resolveDepth(e),!e)throw new RangeError("There is no position after the top-level node");return e==this.depth+1?this.pos:this.path[e*3-1]+this.path[e*3].nodeSize}get textOffset(){return this.pos-this.path[this.path.length-1]}get nodeAfter(){let e=this.parent,t=this.index(this.depth);if(t==e.childCount)return null;let n=this.pos-this.path[this.path.length-1],i=e.child(t);return n?e.child(t).cut(n):i}get nodeBefore(){let e=this.index(this.depth),t=this.pos-this.path[this.path.length-1];return t?this.parent.child(e).cut(0,t):e==0?null:this.parent.child(e-1)}posAtIndex(e,t){t=this.resolveDepth(t);let n=this.path[t*3],i=t==0?0:this.path[t*3-1]+1;for(let s=0;s0;t--)if(this.start(t)<=e&&this.end(t)>=e)return t;return 0}blockRange(e=this,t){if(e.pos=0;n--)if(e.pos<=this.end(n)&&(!t||t(this.node(n))))return new be(this,e,n);return null}sameParent(e){return this.pos-this.parentOffset==e.pos-e.parentOffset}max(e){return e.pos>this.pos?e:this}min(e){return e.pos=0&&t<=e.content.size))throw new RangeError("Position "+t+" out of range");let n=[],i=0,s=t;for(let o=e;;){let{index:l,offset:a}=o.content.findIndex(s),f=s-a;if(n.push(o,l,i+a),!f||(o=o.child(l),o.isText))break;s=f-1,i+=a+1}return new r(t,n,s)}static resolveCached(e,t){let n=Dr.get(e);if(n)for(let s=0;se&&this.nodesBetween(e,t,s=>(n.isInSet(s.marks)&&(i=!0),!i)),i}get isBlock(){return this.type.isBlock}get isTextblock(){return this.type.isTextblock}get inlineContent(){return this.type.inlineContent}get isInline(){return this.type.isInline}get isText(){return this.type.isText}get isLeaf(){return this.type.isLeaf}get isAtom(){return this.type.isAtom}toString(){if(this.type.spec.toDebugString)return this.type.spec.toDebugString(this);let e=this.type.name;return this.content.size&&(e+="("+this.content.toStringInner()+")"),Hr(this.marks,e)}contentMatchAt(e){let t=this.type.contentMatch.matchFragment(this.content,0,e);if(!t)throw new Error("Called contentMatchAt on a node with invalid content");return t}canReplace(e,t,n=y.empty,i=0,s=n.childCount){let o=this.contentMatchAt(e).matchFragment(n,i,s),l=o&&o.matchFragment(this.content,t);if(!l||!l.validEnd)return!1;for(let a=i;at.type.name)}`);this.content.forEach(t=>t.check())}toJSON(){let e={type:this.type.name};for(let t in this.attrs){e.attrs=this.attrs;break}return this.content.size&&(e.content=this.content.toJSON()),this.marks.length&&(e.marks=this.marks.map(t=>t.toJSON())),e}static fromJSON(e,t){if(!t)throw new RangeError("Invalid input for Node.fromJSON");let n;if(t.marks){if(!Array.isArray(t.marks))throw new RangeError("Invalid mark data for Node.fromJSON");n=t.marks.map(e.markFromJSON)}if(t.type=="text"){if(typeof t.text!="string")throw new RangeError("Invalid text node in JSON");return e.text(t.text,n)}let i=y.fromJSON(e,t.content),s=e.nodeType(t.type).create(t.attrs,i,n);return s.type.checkAttrs(s.attrs),s}};Q.prototype.text=void 0;var xn=class r extends Q{constructor(e,t,n,i){if(super(e,t,null,i),!n)throw new RangeError("Empty text nodes are not allowed");this.text=n}toString(){return this.type.spec.toDebugString?this.type.spec.toDebugString(this):Hr(this.marks,JSON.stringify(this.text))}get textContent(){return this.text}textBetween(e,t){return this.text.slice(e,t)}get nodeSize(){return this.text.length}mark(e){return e==this.marks?this:new r(this.type,this.attrs,this.text,e)}withText(e){return e==this.text?this:new r(this.type,this.attrs,e,this.marks)}cut(e=0,t=this.text.length){return e==0&&t==this.text.length?this:this.withText(this.text.slice(e,t))}eq(e){return this.sameMarkup(e)&&this.text==e.text}toJSON(){let e=super.toJSON();return e.text=this.text,e}};function Hr(r,e){for(let t=r.length-1;t>=0;t--)e=r[t].type.name+"("+e+")";return e}var Se=class r{constructor(e){this.validEnd=e,this.next=[],this.wrapCache=[]}static parse(e,t){let n=new bn(e,t);if(n.next==null)return r.empty;let i=Ur(n);n.next&&n.err("Unexpected trailing text");let s=$o(Jo(i));return qo(s,n),s}matchType(e){for(let t=0;tf.createAndFill()));for(let f=0;f=this.next.length)throw new RangeError(`There's no ${e}th edge in this content match`);return this.next[e]}toString(){let e=[];function t(n){e.push(n);for(let i=0;i{let s=i+(n.validEnd?"*":" ")+" ";for(let o=0;o"+e.indexOf(n.next[o].next);return s}).join(` +`)}};Se.empty=new Se(!0);var bn=class{constructor(e,t){this.string=e,this.nodeTypes=t,this.inline=null,this.pos=0,this.tokens=e.split(/\s*(?=\b|\W|$)/),this.tokens[this.tokens.length-1]==""&&this.tokens.pop(),this.tokens[0]==""&&this.tokens.shift()}get next(){return this.tokens[this.pos]}eat(e){return this.next==e&&(this.pos++||!0)}err(e){throw new SyntaxError(e+" (in content expression '"+this.string+"')")}};function Ur(r){let e=[];do e.push(vo(r));while(r.eat("|"));return e.length==1?e[0]:{type:"choice",exprs:e}}function vo(r){let e=[];do e.push(Fo(r));while(r.next&&r.next!=")"&&r.next!="|");return e.length==1?e[0]:{type:"seq",exprs:e}}function Fo(r){let e=Wo(r);for(;;)if(r.eat("+"))e={type:"plus",expr:e};else if(r.eat("*"))e={type:"star",expr:e};else if(r.eat("?"))e={type:"opt",expr:e};else if(r.eat("{"))e=Vo(r,e);else break;return e}function Tr(r){/\D/.test(r.next)&&r.err("Expected number, got '"+r.next+"'");let e=Number(r.next);return r.pos++,e}function Vo(r,e){let t=Tr(r),n=t;return r.eat(",")&&(r.next!="}"?n=Tr(r):n=-1),r.eat("}")||r.err("Unclosed braced range"),{type:"range",min:t,max:n,expr:e}}function Lo(r,e){let t=r.nodeTypes,n=t[e];if(n)return[n];let i=[];for(let s in t){let o=t[s];o.isInGroup(e)&&i.push(o)}return i.length==0&&r.err("No node type or group '"+e+"' found"),i}function Wo(r){if(r.eat("(")){let e=Ur(r);return r.eat(")")||r.err("Missing closing paren"),e}else if(/\W/.test(r.next))r.err("Unexpected token '"+r.next+"'");else{let e=Lo(r,r.next).map(t=>(r.inline==null?r.inline=t.isInline:r.inline!=t.isInline&&r.err("Mixing inline and block content"),{type:"name",value:t}));return r.pos++,e.length==1?e[0]:{type:"choice",exprs:e}}}function Jo(r){let e=[[]];return i(s(r,0),t()),e;function t(){return e.push([])-1}function n(o,l,a){let f={term:a,to:l};return e[o].push(f),f}function i(o,l){o.forEach(a=>a.to=l)}function s(o,l){if(o.type=="choice")return o.exprs.reduce((a,f)=>a.concat(s(f,l)),[]);if(o.type=="seq")for(let a=0;;a++){let f=s(o.exprs[a],l);if(a==o.exprs.length-1)return f;i(f,l=t())}else if(o.type=="star"){let a=t();return n(l,a),i(s(o.expr,a),a),[n(a)]}else if(o.type=="plus"){let a=t();return i(s(o.expr,l),a),i(s(o.expr,a),a),[n(a)]}else{if(o.type=="opt")return[n(l)].concat(s(o.expr,l));if(o.type=="range"){let a=l;for(let f=0;f{r[o].forEach(({term:l,to:a})=>{if(!l)return;let f;for(let c=0;c{f||i.push([l,f=[]]),f.indexOf(c)==-1&&f.push(c)})})});let s=e[n.join(",")]=new Se(n.indexOf(r.length-1)>-1);for(let o=0;o-1}get whitespace(){return this.spec.whitespace||(this.spec.code?"pre":"normal")}hasRequiredAttrs(){for(let e in this.attrs)if(this.attrs[e].isRequired)return!0;return!1}compatibleContent(e){return this==e||this.contentMatch.compatible(e.contentMatch)}computeAttrs(e){return!e&&this.defaultAttrs?this.defaultAttrs:Yr(this.attrs,e)}create(e=null,t,n){if(this.isText)throw new Error("NodeType.create can't construct text nodes");return new Q(this,this.computeAttrs(e),y.from(t),O.setFrom(n))}createChecked(e=null,t,n){return t=y.from(t),this.checkContent(t),new Q(this,this.computeAttrs(e),t,O.setFrom(n))}createAndFill(e=null,t,n){if(e=this.computeAttrs(e),t=y.from(t),t.size){let o=this.contentMatch.fillBefore(t);if(!o)return null;t=o.append(t)}let i=this.contentMatch.matchFragment(t),s=i&&i.fillBefore(y.empty,!0);return s?new Q(this,e,t.append(s),O.setFrom(n)):null}validContent(e){let t=this.contentMatch.matchFragment(e);if(!t||!t.validEnd)return!1;for(let n=0;n-1}allowsMarks(e){if(this.markSet==null)return!0;for(let t=0;tn[s]=new r(s,t,o));let i=t.spec.topNode||"doc";if(!n[i])throw new RangeError("Schema is missing its top node type ('"+i+"')");if(!n.text)throw new RangeError("Every schema needs a 'text' type");for(let s in n.text.attrs)throw new RangeError("The text node type should not have attributes");return n}};function Ko(r,e,t){let n=t.split("|");return i=>{let s=i===null?"null":typeof i;if(n.indexOf(s)<0)throw new RangeError(`Expected value of type ${n} for attribute ${e} on type ${r}, got ${s}`)}}var Sn=class{constructor(e,t,n){this.hasDefault=Object.prototype.hasOwnProperty.call(n,"default"),this.default=n.default,this.validate=typeof n.validate=="string"?Ko(e,t,n.validate):n.validate}get isRequired(){return!this.hasDefault}},Ye=class r{constructor(e,t,n,i){this.name=e,this.rank=t,this.schema=n,this.spec=i,this.attrs=_r(e,i.attrs),this.excluded=null;let s=Gr(this.attrs);this.instance=s?new O(this,s):null}create(e=null){return!e&&this.instance?this.instance:new O(this,Yr(this.attrs,e))}static compile(e,t){let n=Object.create(null),i=0;return e.forEach((s,o)=>n[s]=new r(s,i++,t,o)),n}removeFromSet(e){for(var t=0;t-1}},pt=class{constructor(e){this.linebreakReplacement=null,this.cached=Object.create(null);let t=this.spec={};for(let i in e)t[i]=e[i];t.nodes=pn.from(e.nodes),t.marks=pn.from(e.marks||{}),this.nodes=dt.compile(this.spec.nodes,this),this.marks=Ye.compile(this.spec.marks,this);let n=Object.create(null);for(let i in this.nodes){if(i in this.marks)throw new RangeError(i+" can not be both a node and a mark");let s=this.nodes[i],o=s.spec.content||"",l=s.spec.marks;if(s.contentMatch=n[o]||(n[o]=Se.parse(o,this.nodes)),s.inlineContent=s.contentMatch.inlineContent,s.spec.linebreakReplacement){if(this.linebreakReplacement)throw new RangeError("Multiple linebreak nodes defined");if(!s.isInline||!s.isLeaf)throw new RangeError("Linebreak replacement nodes must be inline leaf nodes");this.linebreakReplacement=s}s.markSet=l=="_"?null:l?Ar(this,l.split(" ")):l==""||!s.inlineContent?[]:null}for(let i in this.marks){let s=this.marks[i],o=s.spec.excludes;s.excluded=o==null?[s]:o==""?[]:Ar(this,o.split(" "))}this.nodeFromJSON=i=>Q.fromJSON(this,i),this.markFromJSON=i=>O.fromJSON(this,i),this.topNodeType=this.nodes[this.spec.topNode||"doc"],this.cached.wrappings=Object.create(null)}node(e,t=null,n,i){if(typeof e=="string")e=this.nodeType(e);else if(e instanceof dt){if(e.schema!=this)throw new RangeError("Node type from different schema used ("+e.name+")")}else throw new RangeError("Invalid node type: "+e);return e.createChecked(t,n,i)}text(e,t){let n=this.nodes.text;return new xn(n,n.defaultAttrs,e,O.setFrom(t))}mark(e,t){return typeof e=="string"&&(e=this.marks[e]),e.create(t)}nodeType(e){let t=this.nodes[e];if(!t)throw new RangeError("Unknown node type: "+e);return t}};function Ar(r,e){let t=[];for(let n=0;n-1)&&t.push(o=a)}if(!o)throw new SyntaxError("Unknown mark type: '"+e[n]+"'")}return t}function Ho(r){return r.tag!=null}function Uo(r){return r.style!=null}var Xe=class r{constructor(e,t){this.schema=e,this.rules=t,this.tags=[],this.styles=[];let n=this.matchedStyles=[];t.forEach(i=>{if(Ho(i))this.tags.push(i);else if(Uo(i)){let s=/[^=]*/.exec(i.style)[0];n.indexOf(s)<0&&n.push(s),this.styles.push(i)}}),this.normalizeLists=!this.tags.some(i=>{if(!/^(ul|ol)\b/.test(i.tag)||!i.node)return!1;let s=e.nodes[i.node];return s.contentMatch.matchType(s)})}parse(e,t={}){let n=new Wt(this,t,!1);return n.addAll(e,O.none,t.from,t.to),n.finish()}parseSlice(e,t={}){let n=new Wt(this,t,!0);return n.addAll(e,O.none,t.from,t.to),x.maxOpen(n.finish())}matchTag(e,t,n){for(let i=n?this.tags.indexOf(n)+1:0;ie.length&&(l.charCodeAt(e.length)!=61||l.slice(e.length+1)!=t))){if(o.getAttrs){let a=o.getAttrs(t);if(a===!1)continue;o.attrs=a||void 0}return o}}}static schemaRules(e){let t=[];function n(i){let s=i.priority==null?50:i.priority,o=0;for(;o{n(o=Rr(o)),o.mark||o.ignore||o.clearMark||(o.mark=i)})}for(let i in e.nodes){let s=e.nodes[i].spec.parseDOM;s&&s.forEach(o=>{n(o=Rr(o)),o.node||o.ignore||o.mark||(o.node=i)})}return t}static fromSchema(e){return e.cached.domParser||(e.cached.domParser=new r(e,r.schemaRules(e)))}},Qr={address:!0,article:!0,aside:!0,blockquote:!0,body:!0,canvas:!0,dd:!0,div:!0,dl:!0,fieldset:!0,figcaption:!0,figure:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,li:!0,noscript:!0,ol:!0,output:!0,p:!0,pre:!0,section:!0,table:!0,tfoot:!0,ul:!0},jo={head:!0,noscript:!0,object:!0,script:!0,style:!0,title:!0},Zr={ol:!0,ul:!0},mt=1,kn=2,ht=4;function Ir(r,e,t){return e!=null?(e?mt:0)|(e==="full"?kn:0):r&&r.whitespace=="pre"?mt|kn:t&~ht}var Ge=class{constructor(e,t,n,i,s,o){this.type=e,this.attrs=t,this.marks=n,this.solid=i,this.options=o,this.content=[],this.activeMarks=O.none,this.match=s||(o&ht?null:e.contentMatch)}findWrapping(e){if(!this.match){if(!this.type)return[];let t=this.type.contentMatch.fillBefore(y.from(e));if(t)this.match=this.type.contentMatch.matchFragment(t);else{let n=this.type.contentMatch,i;return(i=n.findWrapping(e.type))?(this.match=n,i):null}}return this.match.findWrapping(e.type)}finish(e){if(!(this.options&mt)){let n=this.content[this.content.length-1],i;if(n&&n.isText&&(i=/[ \t\r\n\u000c]+$/.exec(n.text))){let s=n;n.text.length==i[0].length?this.content.pop():this.content[this.content.length-1]=s.withText(s.text.slice(0,s.text.length-i[0].length))}}let t=y.from(this.content);return!e&&this.match&&(t=t.append(this.match.fillBefore(y.empty,!0))),this.type?this.type.create(this.attrs,t,this.marks):t}inlineContext(e){return this.type?this.type.inlineContent:this.content.length?this.content[0].isInline:e.parentNode&&!Qr.hasOwnProperty(e.parentNode.nodeName.toLowerCase())}},Wt=class{constructor(e,t,n){this.parser=e,this.options=t,this.isOpen=n,this.open=0,this.localPreserveWS=!1;let i=t.topNode,s,o=Ir(null,t.preserveWhitespace,0)|(n?ht:0);i?s=new Ge(i.type,i.attrs,O.none,!0,t.topMatch||i.type.contentMatch,o):n?s=new Ge(null,null,O.none,!0,null,o):s=new Ge(e.schema.topNodeType,null,O.none,!0,null,o),this.nodes=[s],this.find=t.findPositions,this.needsBlock=!1}get top(){return this.nodes[this.open]}addDOM(e,t){e.nodeType==3?this.addTextNode(e,t):e.nodeType==1&&this.addElement(e,t)}addTextNode(e,t){let n=e.nodeValue,i=this.top,s=i.options&kn?"full":this.localPreserveWS||(i.options&mt)>0,{schema:o}=this.parser;if(s==="full"||i.inlineContext(e)||/[^ \t\r\n\u000c]/.test(n)){if(s)if(s==="full")n=n.replace(/\r\n?/g,` +`);else if(o.linebreakReplacement&&/[\r\n]/.test(n)&&this.top.findWrapping(o.linebreakReplacement.create())){let l=n.split(/\r?\n|\r/);for(let a=0;a!a.clearMark(f)):t=t.concat(this.parser.schema.marks[a.mark].create(a.attrs)),a.consuming===!1)l=a;else break}}return t}addElementByRule(e,t,n,i){let s,o;if(t.node)if(o=this.parser.schema.nodes[t.node],o.isLeaf)this.insertNode(o.create(t.attrs),n,e.nodeName=="BR")||this.leafFallback(e,n);else{let a=this.enter(o,t.attrs||null,n,t.preserveWhitespace);a&&(s=!0,n=a)}else{let a=this.parser.schema.marks[t.mark];n=n.concat(a.create(t.attrs))}let l=this.top;if(o&&o.isLeaf)this.findInside(e);else if(i)this.addElement(e,n,i);else if(t.getContent)this.findInside(e),t.getContent(e,this.parser.schema).forEach(a=>this.insertNode(a,n,!1));else{let a=e;typeof t.contentElement=="string"?a=e.querySelector(t.contentElement):typeof t.contentElement=="function"?a=t.contentElement(e):t.contentElement&&(a=t.contentElement),this.findAround(e,a,!0),this.addAll(a,n),this.findAround(e,a,!1)}s&&this.sync(l)&&this.open--}addAll(e,t,n,i){let s=n||0;for(let o=n?e.childNodes[n]:e.firstChild,l=i==null?null:e.childNodes[i];o!=l;o=o.nextSibling,++s)this.findAtPoint(e,s),this.addDOM(o,t);this.findAtPoint(e,s)}findPlace(e,t,n){let i,s;for(let o=this.open,l=0;o>=0;o--){let a=this.nodes[o],f=a.findWrapping(e);if(f&&(!i||i.length>f.length+l)&&(i=f,s=a,!f.length))break;if(a.solid){if(n)break;l+=2}}if(!i)return null;this.sync(s);for(let o=0;o(o.type?o.type.allowsMarkType(f.type):Pr(f.type,e))?(a=f.addToSet(a),!1):!0),this.nodes.push(new Ge(e,t,a,i,null,l)),this.open++,n}closeExtra(e=!1){let t=this.nodes.length-1;if(t>this.open){for(;t>this.open;t--)this.nodes[t-1].content.push(this.nodes[t].finish(e));this.nodes.length=this.open+1}}finish(){return this.open=0,this.closeExtra(this.isOpen),this.nodes[0].finish(!!(this.isOpen||this.options.topOpen))}sync(e){for(let t=this.open;t>=0;t--){if(this.nodes[t]==e)return this.open=t,!0;this.localPreserveWS&&(this.nodes[t].options|=mt)}return!1}get currentPos(){this.closeExtra();let e=0;for(let t=this.open;t>=0;t--){let n=this.nodes[t].content;for(let i=n.length-1;i>=0;i--)e+=n[i].nodeSize;t&&e++}return e}findAtPoint(e,t){if(this.find)for(let n=0;n-1)return e.split(/\s*\|\s*/).some(this.matchesContext,this);let t=e.split("/"),n=this.options.context,i=!this.isOpen&&(!n||n.parent.type==this.nodes[0].type),s=-(n?n.depth+1:0)+(i?0:1),o=(l,a)=>{for(;l>=0;l--){let f=t[l];if(f==""){if(l==t.length-1||l==0)continue;for(;a>=s;a--)if(o(l-1,a))return!0;return!1}else{let c=a>0||a==0&&i?this.nodes[a].type:n&&a>=s?n.node(a-s).type:null;if(!c||c.name!=f&&!c.isInGroup(f))return!1;a--}}return!0};return o(t.length-1,this.open)}textblockFromContext(){let e=this.options.context;if(e)for(let t=e.depth;t>=0;t--){let n=e.node(t).contentMatchAt(e.indexAfter(t)).defaultType;if(n&&n.isTextblock&&n.defaultAttrs)return n}for(let t in this.parser.schema.nodes){let n=this.parser.schema.nodes[t];if(n.isTextblock&&n.defaultAttrs)return n}}};function Go(r){for(let e=r.firstChild,t=null;e;e=e.nextSibling){let n=e.nodeType==1?e.nodeName.toLowerCase():null;n&&Zr.hasOwnProperty(n)&&t?(t.appendChild(e),e=t):n=="li"?t=e:n&&(t=null)}}function Yo(r,e){return(r.matches||r.msMatchesSelector||r.webkitMatchesSelector||r.mozMatchesSelector).call(r,e)}function Rr(r){let e={};for(let t in r)e[t]=r[t];return e}function Pr(r,e){let t=e.schema.nodes;for(let n in t){let i=t[n];if(!i.allowsMarkType(r))continue;let s=[],o=l=>{s.push(l);for(let a=0;a{if(s.length||o.marks.length){let l=0,a=0;for(;l=0;i--){let s=this.serializeMark(e.marks[i],e.isInline,t);s&&((s.contentDOM||s.dom).appendChild(n),n=s.dom)}return n}serializeMark(e,t,n={}){let i=this.marks[e.type.name];return i&&Ft(vt(n),i(e,t),null,e.attrs)}static renderSpec(e,t,n=null,i){return typeof t=="string"?{dom:e.createTextNode(t)}:Ft(e,t,n,i)}static fromSchema(e){return e.cached.domSerializer||(e.cached.domSerializer=new r(this.nodesFromSchema(e),this.marksFromSchema(e)))}static nodesFromSchema(e){let t=Br(e.nodes);return t.text||(t.text=n=>n.text),t}static marksFromSchema(e){return Br(e.marks)}};function Br(r){let e={};for(let t in r){let n=r[t].spec.toDOM;n&&(e[t]=n)}return e}function vt(r){return r.document||window.document}var zr=new WeakMap;function Xo(r){let e=zr.get(r);return e===void 0&&zr.set(r,e=_o(r)),e}function _o(r){let e=null;function t(n){if(n&&typeof n=="object")if(Array.isArray(n))if(typeof n[0]=="string")e||(e=[]),e.push(n);else for(let i=0;i-1)throw new RangeError("Using an array from an attribute object as a DOM spec. This may be an attempted cross site scripting attack.");let o=i.indexOf(" ");o>0&&(t=i.slice(0,o),i=i.slice(o+1));let l,a=t?r.createElementNS(t,i):r.createElement(i),f=e[1],c=1;if(f&&typeof f=="object"&&f.nodeType==null&&!Array.isArray(f)){c=2;for(let h in f)if(f[h]!=null){let u=h.indexOf(" ");u>0?a.setAttributeNS(h.slice(0,u),h.slice(u+1),f[h]):h=="style"&&a.style?a.style.cssText=f[h]:a.setAttribute(h,f[h])}}for(let h=c;hc)throw new RangeError("Content hole must be the only child of its parent node");return{dom:a,contentDOM:a}}else if(typeof u=="string")a.appendChild(r.createTextNode(u));else{let{dom:d,contentDOM:p}=Ft(r,u,t,n);if(a.appendChild(d),p){if(l)throw new RangeError("Multiple content holes");l=p}}}return{dom:a,contentDOM:l}}var gi={};j(gi,{AddMarkStep:()=>Ze,AddNodeMarkStep:()=>et,AttrStep:()=>xt,DocAttrStep:()=>bt,MapResult:()=>_e,Mapping:()=>Qe,RemoveMarkStep:()=>Ce,RemoveNodeMarkStep:()=>Pe,ReplaceAroundStep:()=>E,ReplaceStep:()=>F,Step:()=>T,StepMap:()=>ie,StepResult:()=>I,Transform:()=>St,TransformError:()=>Be,canJoin:()=>Z,canSplit:()=>he,dropPoint:()=>kt,findWrapping:()=>ze,insertPoint:()=>hi,joinPoint:()=>$t,liftTarget:()=>Me,replaceStep:()=>tt});var ri=65535,ii=Math.pow(2,16);function Qo(r,e){return r+e*ii}function ti(r){return r&ri}function Zo(r){return(r-(r&ri))/ii}var si=1,oi=2,Jt=4,li=8,_e=class{constructor(e,t,n){this.pos=e,this.delInfo=t,this.recover=n}get deleted(){return(this.delInfo&li)>0}get deletedBefore(){return(this.delInfo&(si|Jt))>0}get deletedAfter(){return(this.delInfo&(oi|Jt))>0}get deletedAcross(){return(this.delInfo&Jt)>0}},ie=class r{constructor(e,t=!1){if(this.ranges=e,this.inverted=t,!e.length&&r.empty)return r.empty}recover(e){let t=0,n=ti(e);if(!this.inverted)for(let i=0;ie)break;let f=this.ranges[l+s],c=this.ranges[l+o],h=a+f;if(e<=h){let u=f?e==a?-1:e==h?1:t:t,d=a+i+(u<0?0:c);if(n)return d;let p=e==(t<0?a:h)?null:Qo(l/3,e-a),m=e==a?oi:e==h?si:Jt;return(t<0?e!=a:e!=h)&&(m|=li),new _e(d,m,p)}i+=c-f}return n?e+i:new _e(e+i,0,null)}touches(e,t){let n=0,i=ti(t),s=this.inverted?2:1,o=this.inverted?1:2;for(let l=0;le)break;let f=this.ranges[l+s],c=a+f;if(e<=c&&l==i*3)return!0;n+=this.ranges[l+o]-f}return!1}forEach(e){let t=this.inverted?2:1,n=this.inverted?1:2;for(let i=0,s=0;i=0;t--){let i=e.getMirror(t);this.appendMap(e._maps[t].invert(),i!=null&&i>t?n-i-1:void 0)}}invert(){let e=new r;return e.appendMappingInverted(this),e}map(e,t=1){if(this.mirror)return this._map(e,t,!0);for(let n=this.from;ns&&a!o.isAtom||!l.type.allowsMarkType(this.mark.type)?o:o.mark(this.mark.addToSet(o.marks)),i),t.openStart,t.openEnd);return I.fromReplace(e,this.from,this.to,s)}invert(){return new Ce(this.from,this.to,this.mark)}map(e){let t=e.mapResult(this.from,1),n=e.mapResult(this.to,-1);return t.deleted&&n.deleted||t.pos>=n.pos?null:new r(t.pos,n.pos,this.mark)}merge(e){return e instanceof r&&e.mark.eq(this.mark)&&this.from<=e.to&&this.to>=e.from?new r(Math.min(this.from,e.from),Math.max(this.to,e.to),this.mark):null}toJSON(){return{stepType:"addMark",mark:this.mark.toJSON(),from:this.from,to:this.to}}static fromJSON(e,t){if(typeof t.from!="number"||typeof t.to!="number")throw new RangeError("Invalid input for AddMarkStep.fromJSON");return new r(t.from,t.to,e.markFromJSON(t.mark))}};T.jsonID("addMark",Ze);var Ce=class r extends T{constructor(e,t,n){super(),this.from=e,this.to=t,this.mark=n}apply(e){let t=e.slice(this.from,this.to),n=new x(Dn(t.content,i=>i.mark(this.mark.removeFromSet(i.marks)),e),t.openStart,t.openEnd);return I.fromReplace(e,this.from,this.to,n)}invert(){return new Ze(this.from,this.to,this.mark)}map(e){let t=e.mapResult(this.from,1),n=e.mapResult(this.to,-1);return t.deleted&&n.deleted||t.pos>=n.pos?null:new r(t.pos,n.pos,this.mark)}merge(e){return e instanceof r&&e.mark.eq(this.mark)&&this.from<=e.to&&this.to>=e.from?new r(Math.min(this.from,e.from),Math.max(this.to,e.to),this.mark):null}toJSON(){return{stepType:"removeMark",mark:this.mark.toJSON(),from:this.from,to:this.to}}static fromJSON(e,t){if(typeof t.from!="number"||typeof t.to!="number")throw new RangeError("Invalid input for RemoveMarkStep.fromJSON");return new r(t.from,t.to,e.markFromJSON(t.mark))}};T.jsonID("removeMark",Ce);var et=class r extends T{constructor(e,t){super(),this.pos=e,this.mark=t}apply(e){let t=e.nodeAt(this.pos);if(!t)return I.fail("No node at mark step's position");let n=t.type.create(t.attrs,null,this.mark.addToSet(t.marks));return I.fromReplace(e,this.pos,this.pos+1,new x(y.from(n),0,t.isLeaf?0:1))}invert(e){let t=e.nodeAt(this.pos);if(t){let n=this.mark.addToSet(t.marks);if(n.length==t.marks.length){for(let i=0;in.pos?null:new r(t.pos,n.pos,i,s,this.slice,this.insert,this.structure)}toJSON(){let e={stepType:"replaceAround",from:this.from,to:this.to,gapFrom:this.gapFrom,gapTo:this.gapTo,insert:this.insert};return this.slice.size&&(e.slice=this.slice.toJSON()),this.structure&&(e.structure=!0),e}static fromJSON(e,t){if(typeof t.from!="number"||typeof t.to!="number"||typeof t.gapFrom!="number"||typeof t.gapTo!="number"||typeof t.insert!="number")throw new RangeError("Invalid input for ReplaceAroundStep.fromJSON");return new r(t.from,t.to,t.gapFrom,t.gapTo,x.fromJSON(e,t.slice),t.insert,!!t.structure)}};T.jsonID("replaceAround",E);function wn(r,e,t){let n=r.resolve(e),i=t-e,s=n.depth;for(;i>0&&s>0&&n.indexAfter(s)==n.node(s).childCount;)s--,i--;if(i>0){let o=n.node(s).maybeChild(n.indexAfter(s));for(;i>0;){if(!o||o.isLeaf)return!0;o=o.firstChild,i--}}return!1}function el(r,e,t,n){let i=[],s=[],o,l;r.doc.nodesBetween(e,t,(a,f,c)=>{if(!a.isInline)return;let h=a.marks;if(!n.isInSet(h)&&c.type.allowsMarkType(n.type)){let u=Math.max(f,e),d=Math.min(f+a.nodeSize,t),p=n.addToSet(h);for(let m=0;mr.step(a)),s.forEach(a=>r.step(a))}function tl(r,e,t,n){let i=[],s=0;r.doc.nodesBetween(e,t,(o,l)=>{if(!o.isInline)return;s++;let a=null;if(n instanceof Ye){let f=o.marks,c;for(;c=n.isInSet(f);)(a||(a=[])).push(c),f=c.removeFromSet(f)}else n?n.isInSet(o.marks)&&(a=[n]):a=o.marks;if(a&&a.length){let f=Math.min(l+o.nodeSize,t);for(let c=0;cr.step(new Ce(o.from,o.to,o.style)))}function Tn(r,e,t,n=t.contentMatch,i=!0){let s=r.doc.nodeAt(e),o=[],l=e+1;for(let a=0;a=0;a--)r.step(o[a])}function nl(r,e,t){return(e==0||r.canReplace(e,r.childCount))&&(t==r.childCount||r.canReplace(0,t))}function Me(r){let t=r.parent.content.cutByIndex(r.startIndex,r.endIndex);for(let n=r.depth,i=0,s=0;;--n){let o=r.$from.node(n),l=r.$from.index(n)+i,a=r.$to.indexAfter(n)-s;if(nt;p--)m||n.index(p)>0?(m=!0,c=y.from(n.node(p).copy(c)),h++):a--;let u=y.empty,d=0;for(let p=s,m=!1;p>t;p--)m||i.after(p+1)=0;o--){if(n.size){let l=t[o].type.contentMatch.matchFragment(n);if(!l||!l.validEnd)throw new RangeError("Wrapper type given to Transform.wrap does not form valid content of its parent wrapper")}n=y.from(t[o].type.create(t[o].attrs,n))}let i=e.start,s=e.end;r.step(new E(i,s,i,s,new x(n,0,0),t.length,!0))}function ll(r,e,t,n,i){if(!n.isTextblock)throw new RangeError("Type given to setBlockType should be a textblock");let s=r.steps.length;r.doc.nodesBetween(e,t,(o,l)=>{let a=typeof i=="function"?i(o):i;if(o.isTextblock&&!o.hasMarkup(n,a)&&al(r.doc,r.mapping.slice(s).map(l),n)){let f=null;if(n.schema.linebreakReplacement){let d=n.whitespace=="pre",p=!!n.contentMatch.matchType(n.schema.linebreakReplacement);d&&!p?f=!1:!d&&p&&(f=!0)}f===!1&&fi(r,o,l,s),Tn(r,r.mapping.slice(s).map(l,1),n,void 0,f===null);let c=r.mapping.slice(s),h=c.map(l,1),u=c.map(l+o.nodeSize,1);return r.step(new E(h,u,h+1,u-1,new x(y.from(n.create(a,null,o.marks)),0,0),1,!0)),f===!0&&ai(r,o,l,s),!1}})}function ai(r,e,t,n){e.forEach((i,s)=>{if(i.isText){let o,l=/\r?\n|\r/g;for(;o=l.exec(i.text);){let a=r.mapping.slice(n).map(t+1+s+o.index);r.replaceWith(a,a+1,e.type.schema.linebreakReplacement.create())}}})}function fi(r,e,t,n){e.forEach((i,s)=>{if(i.type==i.type.schema.linebreakReplacement){let o=r.mapping.slice(n).map(t+1+s);r.replaceWith(o,o+1,e.type.schema.text(` +`))}})}function al(r,e,t){let n=r.resolve(e),i=n.index();return n.parent.canReplaceWith(i,i+1,t)}function fl(r,e,t,n,i){let s=r.doc.nodeAt(e);if(!s)throw new RangeError("No node at given position");t||(t=s.type);let o=t.create(n,null,i||s.marks);if(s.isLeaf)return r.replaceWith(e,e+s.nodeSize,o);if(!t.validContent(s.content))throw new RangeError("Invalid content for node type "+t.name);r.step(new E(e,e+s.nodeSize,e+1,e+s.nodeSize-1,new x(y.from(o),0,0),1,!0))}function he(r,e,t=1,n){let i=r.resolve(e),s=i.depth-t,o=n&&n[n.length-1]||i.parent;if(s<0||i.parent.type.spec.isolating||!i.parent.canReplace(i.index(),i.parent.childCount)||!o.type.validContent(i.parent.content.cutByIndex(i.index(),i.parent.childCount)))return!1;for(let f=i.depth-1,c=t-2;f>s;f--,c--){let h=i.node(f),u=i.index(f);if(h.type.spec.isolating)return!1;let d=h.content.cutByIndex(u,h.childCount),p=n&&n[c+1];p&&(d=d.replaceChild(0,p.type.create(p.attrs)));let m=n&&n[c]||h;if(!h.canReplace(u+1,h.childCount)||!m.type.validContent(d))return!1}let l=i.indexAfter(s),a=n&&n[0];return i.node(s).canReplaceWith(l,l,a?a.type:i.node(s+1).type)}function cl(r,e,t=1,n){let i=r.doc.resolve(e),s=y.empty,o=y.empty;for(let l=i.depth,a=i.depth-t,f=t-1;l>a;l--,f--){s=y.from(i.node(l).copy(s));let c=n&&n[f];o=y.from(c?c.type.create(c.attrs,o):i.node(l).copy(o))}r.step(new F(e,e,new x(s.append(o),t,t),!0))}function Z(r,e){let t=r.resolve(e),n=t.index();return ci(t.nodeBefore,t.nodeAfter)&&t.parent.canReplace(n,n+1)}function hl(r,e){e.content.size||r.type.compatibleContent(e.type);let t=r.contentMatchAt(r.childCount),{linebreakReplacement:n}=r.type.schema;for(let i=0;i0?(s=n.node(i+1),l++,o=n.node(i).maybeChild(l)):(s=n.node(i).maybeChild(l-1),o=n.node(i+1)),s&&!s.isTextblock&&ci(s,o)&&n.node(i).canReplace(l,l+1))return e;if(i==0)break;e=t<0?n.before(i):n.after(i)}}function ul(r,e,t){let n=null,{linebreakReplacement:i}=r.doc.type.schema,s=r.doc.resolve(e-t),o=s.node().type;if(i&&o.inlineContent){let c=o.whitespace=="pre",h=!!o.contentMatch.matchType(i);c&&!h?n=!1:!c&&h&&(n=!0)}let l=r.steps.length;if(n===!1){let c=r.doc.resolve(e+t);fi(r,c.node(),c.before(),l)}o.inlineContent&&Tn(r,e+t-1,o,s.node().contentMatchAt(s.index()),n==null);let a=r.mapping.slice(l),f=a.map(e-t);if(r.step(new F(f,a.map(e+t,-1),x.empty,!0)),n===!0){let c=r.doc.resolve(f);ai(r,c.node(),c.before(),r.steps.length)}return r}function hi(r,e,t){let n=r.resolve(e);if(n.parent.canReplaceWith(n.index(),n.index(),t))return e;if(n.parentOffset==0)for(let i=n.depth-1;i>=0;i--){let s=n.index(i);if(n.node(i).canReplaceWith(s,s,t))return n.before(i+1);if(s>0)return null}if(n.parentOffset==n.parent.content.size)for(let i=n.depth-1;i>=0;i--){let s=n.indexAfter(i);if(n.node(i).canReplaceWith(s,s,t))return n.after(i+1);if(s=0;o--){let l=o==n.depth?0:n.pos<=(n.start(o+1)+n.end(o+1))/2?-1:1,a=n.index(o)+(l>0?1:0),f=n.node(o),c=!1;if(s==1)c=f.canReplace(a,a,i);else{let h=f.contentMatchAt(a).findWrapping(i.firstChild.type);c=h&&f.canReplaceWith(a,a,h[0])}if(c)return l==0?n.pos:l<0?n.before(o+1):n.after(o+1)}return null}function tt(r,e,t=e,n=x.empty){if(e==t&&!n.size)return null;let i=r.resolve(e),s=r.resolve(t);return ui(i,s,n)?new F(e,t,n):new Nn(i,s,n).fit()}function ui(r,e,t){return!t.openStart&&!t.openEnd&&r.start()==e.start()&&r.parent.canReplace(r.index(),e.index(),t.content)}var Nn=class{constructor(e,t,n){this.$from=e,this.$to=t,this.unplaced=n,this.frontier=[],this.placed=y.empty;for(let i=0;i<=e.depth;i++){let s=e.node(i);this.frontier.push({type:s.type,match:s.contentMatchAt(e.indexAfter(i))})}for(let i=e.depth;i>0;i--)this.placed=y.from(e.node(i).copy(this.placed))}get depth(){return this.frontier.length-1}fit(){for(;this.unplaced.size;){let f=this.findFittable();f?this.placeNodes(f):this.openMore()||this.dropNode()}let e=this.mustMoveInline(),t=this.placed.size-this.depth-this.$from.depth,n=this.$from,i=this.close(e<0?this.$to:n.doc.resolve(e));if(!i)return null;let s=this.placed,o=n.depth,l=i.depth;for(;o&&l&&s.childCount==1;)s=s.firstChild.content,o--,l--;let a=new x(s,o,l);return e>-1?new E(n.pos,e,this.$to.pos,this.$to.end(),a,t):a.size||n.pos!=this.$to.pos?new F(n.pos,i.pos,a):null}findFittable(){let e=this.unplaced.openStart;for(let t=this.unplaced.content,n=0,i=this.unplaced.openEnd;n1&&(i=0),s.type.spec.isolating&&i<=n){e=n;break}t=s.content}for(let t=1;t<=2;t++)for(let n=t==1?e:this.unplaced.openStart;n>=0;n--){let i,s=null;n?(s=Mn(this.unplaced.content,n-1).firstChild,i=s.content):i=this.unplaced.content;let o=i.firstChild;for(let l=this.depth;l>=0;l--){let{type:a,match:f}=this.frontier[l],c,h=null;if(t==1&&(o?f.matchType(o.type)||(h=f.fillBefore(y.from(o),!1)):s&&a.compatibleContent(s.type)))return{sliceDepth:n,frontierDepth:l,parent:s,inject:h};if(t==2&&o&&(c=f.findWrapping(o.type)))return{sliceDepth:n,frontierDepth:l,parent:s,wrap:c};if(s&&f.matchType(s.type))break}}}openMore(){let{content:e,openStart:t,openEnd:n}=this.unplaced,i=Mn(e,t);return!i.childCount||i.firstChild.isLeaf?!1:(this.unplaced=new x(e,t+1,Math.max(n,i.size+t>=e.size-n?t+1:0)),!0)}dropNode(){let{content:e,openStart:t,openEnd:n}=this.unplaced,i=Mn(e,t);if(i.childCount<=1&&t>0){let s=e.size-t<=t+i.size;this.unplaced=new x(gt(e,t-1,1),t-1,s?t-1:n)}else this.unplaced=new x(gt(e,t,1),t,n)}placeNodes({sliceDepth:e,frontierDepth:t,parent:n,inject:i,wrap:s}){for(;this.depth>t;)this.closeFrontierNode();if(s)for(let m=0;m1||a==0||m.content.size)&&(h=g,c.push(di(m.mark(u.allowedMarks(m.marks)),f==1?a:0,f==l.childCount?d:-1)))}let p=f==l.childCount;p||(d=-1),this.placed=yt(this.placed,t,y.from(c)),this.frontier[t].match=h,p&&d<0&&n&&n.type==this.frontier[this.depth].type&&this.frontier.length>1&&this.closeFrontierNode();for(let m=0,g=l;m1&&i==this.$to.end(--n);)++i;return i}findCloseLevel(e){e:for(let t=Math.min(this.depth,e.depth);t>=0;t--){let{match:n,type:i}=this.frontier[t],s=t=0;l--){let{match:a,type:f}=this.frontier[l],c=On(e,l,f,a,!0);if(!c||c.childCount)continue e}return{depth:t,fit:o,move:s?e.doc.resolve(e.after(t+1)):e}}}}close(e){let t=this.findCloseLevel(e);if(!t)return null;for(;this.depth>t.depth;)this.closeFrontierNode();t.fit.childCount&&(this.placed=yt(this.placed,t.depth,t.fit)),e=t.move;for(let n=t.depth+1;n<=e.depth;n++){let i=e.node(n),s=i.type.contentMatch.fillBefore(i.content,!0,e.index(n));this.openFrontierNode(i.type,i.attrs,s)}return e}openFrontierNode(e,t=null,n){let i=this.frontier[this.depth];i.match=i.match.matchType(e),this.placed=yt(this.placed,this.depth,y.from(e.create(t,n))),this.frontier.push({type:e,match:e.contentMatch})}closeFrontierNode(){let t=this.frontier.pop().match.fillBefore(y.empty,!0);t.childCount&&(this.placed=yt(this.placed,this.frontier.length,t))}};function gt(r,e,t){return e==0?r.cutByIndex(t,r.childCount):r.replaceChild(0,r.firstChild.copy(gt(r.firstChild.content,e-1,t)))}function yt(r,e,t){return e==0?r.append(t):r.replaceChild(r.childCount-1,r.lastChild.copy(yt(r.lastChild.content,e-1,t)))}function Mn(r,e){for(let t=0;t1&&(n=n.replaceChild(0,di(n.firstChild,e-1,n.childCount==1?t-1:0))),e>0&&(n=r.type.contentMatch.fillBefore(n).append(n),t<=0&&(n=n.append(r.type.contentMatch.matchFragment(n).fillBefore(y.empty,!0)))),r.copy(n)}function On(r,e,t,n,i){let s=r.node(e),o=i?r.indexAfter(e):r.index(e);if(o==s.childCount&&!t.compatibleContent(s.type))return null;let l=n.fillBefore(s.content,!0,o);return l&&!dl(t,s.content,o)?l:null}function dl(r,e,t){for(let n=t;n0;u--,d--){let p=i.node(u).type.spec;if(p.defining||p.definingAsContext||p.isolating)break;o.indexOf(u)>-1?l=u:i.before(u)==d&&o.splice(1,0,-u)}let a=o.indexOf(l),f=[],c=n.openStart;for(let u=n.content,d=0;;d++){let p=u.firstChild;if(f.push(p),d==n.openStart)break;u=p.content}for(let u=c-1;u>=0;u--){let d=f[u],p=pl(d.type);if(p&&!d.sameMarkup(i.node(Math.abs(l)-1)))c=u;else if(p||!d.type.isTextblock)break}for(let u=n.openStart;u>=0;u--){let d=(u+c+1)%(n.openStart+1),p=f[d];if(p)for(let m=0;m=0&&(r.replace(e,t,n),!(r.steps.length>h));u--){let d=o[u];d<0||(e=i.before(d),t=s.after(d))}}function pi(r,e,t,n,i){if(en){let s=i.contentMatchAt(0),o=s.fillBefore(r).append(r);r=o.append(s.matchFragment(o).fillBefore(y.empty,!0))}return r}function gl(r,e,t,n){if(!n.isInline&&e==t&&r.doc.resolve(e).parent.content.size){let i=hi(r.doc,e,n.type);i!=null&&(e=t=i)}r.replaceRange(e,t,new x(y.from(n),0,0))}function yl(r,e,t){let n=r.doc.resolve(e),i=r.doc.resolve(t);if(n.parent.isTextblock&&i.parent.isTextblock&&n.start()!=i.start()&&n.parentOffset==0&&i.parentOffset==0){let o=n.sharedDepth(t),l=!1;for(let a=n.depth;a>o;a--)n.node(a).type.spec.isolating&&(l=!0);for(let a=i.depth;a>o;a--)i.node(a).type.spec.isolating&&(l=!0);if(!l){for(let a=n.depth;a>0&&e==n.start(a);a--)e=n.before(a);for(let a=i.depth;a>0&&t==i.start(a);a--)t=i.before(a);n=r.doc.resolve(e),i=r.doc.resolve(t)}}let s=mi(n,i);for(let o=0;o0&&(a||n.node(l-1).canReplace(n.index(l-1),i.indexAfter(l-1))))return r.delete(n.before(l),i.after(l))}for(let o=1;o<=n.depth&&o<=i.depth;o++)if(e-n.start(o)==n.depth-o&&t>n.end(o)&&i.end(o)-t!=i.depth-o&&n.start(o-1)==i.start(o-1)&&n.node(o-1).canReplace(n.index(o-1),i.index(o-1)))return r.delete(n.before(o),t);r.delete(e,t)}function mi(r,e){let t=[],n=Math.min(r.depth,e.depth);for(let i=n;i>=0;i--){let s=r.start(i);if(se.pos+(e.depth-i)||r.node(i).type.spec.isolating||e.node(i).type.spec.isolating)break;(s==e.start(i)||i==r.depth&&i==e.depth&&r.parent.inlineContent&&e.parent.inlineContent&&i&&e.start(i-1)==s-1)&&t.push(i)}return t}var xt=class r extends T{constructor(e,t,n){super(),this.pos=e,this.attr=t,this.value=n}apply(e){let t=e.nodeAt(this.pos);if(!t)return I.fail("No node at attribute step's position");let n=Object.create(null);for(let s in t.attrs)n[s]=t.attrs[s];n[this.attr]=this.value;let i=t.type.create(n,null,t.marks);return I.fromReplace(e,this.pos,this.pos+1,new x(y.from(i),0,t.isLeaf?0:1))}getMap(){return ie.empty}invert(e){return new r(this.pos,this.attr,e.nodeAt(this.pos).attrs[this.attr])}map(e){let t=e.mapResult(this.pos,1);return t.deletedAfter?null:new r(t.pos,this.attr,this.value)}toJSON(){return{stepType:"attr",pos:this.pos,attr:this.attr,value:this.value}}static fromJSON(e,t){if(typeof t.pos!="number"||typeof t.attr!="string")throw new RangeError("Invalid input for AttrStep.fromJSON");return new r(t.pos,t.attr,t.value)}};T.jsonID("attr",xt);var bt=class r extends T{constructor(e,t){super(),this.attr=e,this.value=t}apply(e){let t=Object.create(null);for(let i in e.attrs)t[i]=e.attrs[i];t[this.attr]=this.value;let n=e.type.create(t,e.content,e.marks);return I.ok(n)}getMap(){return ie.empty}invert(e){return new r(this.attr,e.attrs[this.attr])}map(e){return this}toJSON(){return{stepType:"docAttr",attr:this.attr,value:this.value}}static fromJSON(e,t){if(typeof t.attr!="string")throw new RangeError("Invalid input for DocAttrStep.fromJSON");return new r(t.attr,t.value)}};T.jsonID("docAttr",bt);var Be=class extends Error{};Be=function r(e){let t=Error.call(this,e);return t.__proto__=r.prototype,t};Be.prototype=Object.create(Error.prototype);Be.prototype.constructor=Be;Be.prototype.name="TransformError";var St=class{constructor(e){this.doc=e,this.steps=[],this.docs=[],this.mapping=new Qe}get before(){return this.docs.length?this.docs[0]:this.doc}step(e){let t=this.maybeStep(e);if(t.failed)throw new Be(t.failed);return this}maybeStep(e){let t=e.apply(this.doc);return t.failed||this.addStep(e,t.doc),t}get docChanged(){return this.steps.length>0}changedRange(){let e=1e9,t=-1e9;for(let n=0;n{e=Math.min(e,l),t=Math.max(t,a)})}return e==1e9?null:{from:e,to:t}}addStep(e,t){this.docs.push(this.doc),this.steps.push(e),this.mapping.appendMap(e.getMap()),this.doc=t}replace(e,t=e,n=x.empty){let i=tt(this.doc,e,t,n);return i&&this.step(i),this}replaceWith(e,t,n){return this.replace(e,t,new x(y.from(n),0,0))}delete(e,t){return this.replace(e,t,x.empty)}insert(e,t){return this.replaceWith(e,e,t)}replaceRange(e,t,n){return ml(this,e,t,n),this}replaceRangeWith(e,t,n){return gl(this,e,t,n),this}deleteRange(e,t){return yl(this,e,t),this}lift(e,t){return rl(this,e,t),this}join(e,t=1){return ul(this,e,t),this}wrap(e,t){return ol(this,e,t),this}setBlockType(e,t=e,n,i=null){return ll(this,e,t,n,i),this}setNodeMarkup(e,t,n=null,i){return fl(this,e,t,n,i),this}setNodeAttribute(e,t,n){return this.step(new xt(e,t,n)),this}setDocAttribute(e,t){return this.step(new bt(e,t)),this}addNodeMark(e,t){return this.step(new et(e,t)),this}removeNodeMark(e,t){let n=this.doc.nodeAt(e);if(!n)throw new RangeError("No node at position "+e);if(t instanceof O)t.isInSet(n.marks)&&this.step(new Pe(e,t));else{let i=n.marks,s,o=[];for(;s=t.isInSet(i);)o.push(new Pe(e,s)),i=s.removeFromSet(i);for(let l=o.length-1;l>=0;l--)this.step(o[l])}return this}split(e,t=1,n){return cl(this,e,t,n),this}addMark(e,t,n){return el(this,e,t,n),this}removeMark(e,t,n){return tl(this,e,t,n),this}clearIncompatible(e,t,n){return Tn(this,e,t,n),this}};var En=Object.create(null),k=class{constructor(e,t,n){this.$anchor=e,this.$head=t,this.ranges=n||[new Fe(e.min(t),e.max(t))]}get anchor(){return this.$anchor.pos}get head(){return this.$head.pos}get from(){return this.$from.pos}get to(){return this.$to.pos}get $from(){return this.ranges[0].$from}get $to(){return this.ranges[0].$to}get empty(){let e=this.ranges;for(let t=0;t=0;s--){let o=t<0?nt(e.node(0),e.node(s),e.before(s+1),e.index(s),t,n):nt(e.node(0),e.node(s),e.after(s+1),e.index(s)+1,t,n);if(o)return o}return null}static near(e,t=1){return this.findFrom(e,t)||this.findFrom(e,-t)||new H(e.node(0))}static atStart(e){return nt(e,e,0,0,1)||new H(e)}static atEnd(e){return nt(e,e,e.content.size,e.childCount,-1)||new H(e)}static fromJSON(e,t){if(!t||!t.type)throw new RangeError("Invalid input for Selection.fromJSON");let n=En[t.type];if(!n)throw new RangeError(`No selection type ${t.type} defined`);return n.fromJSON(e,t)}static jsonID(e,t){if(e in En)throw new RangeError("Duplicate use of selection JSON ID "+e);return En[e]=t,t.prototype.jsonID=e,t}getBookmark(){return C.between(this.$anchor,this.$head).getBookmark()}};k.prototype.visible=!0;var Fe=class{constructor(e,t){this.$from=e,this.$to=t}},yi=!1;function xi(r){!yi&&!r.parent.inlineContent&&(yi=!0,console.warn("TextSelection endpoint not pointing into a node with inline content ("+r.parent.type.name+")"))}var C=class r extends k{constructor(e,t=e){xi(e),xi(t),super(e,t)}get $cursor(){return this.$anchor.pos==this.$head.pos?this.$head:null}map(e,t){let n=e.resolve(t.map(this.head));if(!n.parent.inlineContent)return k.near(n);let i=e.resolve(t.map(this.anchor));return new r(i.parent.inlineContent?i:n,n)}replace(e,t=x.empty){if(super.replace(e,t),t==x.empty){let n=this.$from.marksAcross(this.$to);n&&e.ensureMarks(n)}}eq(e){return e instanceof r&&e.anchor==this.anchor&&e.head==this.head}getBookmark(){return new Kt(this.anchor,this.head)}toJSON(){return{type:"text",anchor:this.anchor,head:this.head}}static fromJSON(e,t){if(typeof t.anchor!="number"||typeof t.head!="number")throw new RangeError("Invalid input for TextSelection.fromJSON");return new r(e.resolve(t.anchor),e.resolve(t.head))}static create(e,t,n=t){let i=e.resolve(t);return new this(i,n==t?i:e.resolve(n))}static between(e,t,n){let i=e.pos-t.pos;if((!n||i)&&(n=i>=0?1:-1),!t.parent.inlineContent){let s=k.findFrom(t,n,!0)||k.findFrom(t,-n,!0);if(s)t=s.$head;else return k.near(t,n)}return e.parent.inlineContent||(i==0?e=t:(e=(k.findFrom(e,-n,!0)||k.findFrom(e,n,!0)).$anchor,e.pos0?0:1);i>0?o=0;o+=i){let l=e.child(o);if(l.isAtom){if(!s&&S.isSelectable(l))return S.create(r,t-(i<0?l.nodeSize:0))}else{let a=nt(r,l,t+i,i<0?l.childCount:0,i,s);if(a)return a}t+=l.nodeSize*i}return null}function bi(r,e,t){let n=r.steps.length-1;if(n{o==null&&(o=c)}),r.setSelection(k.near(r.doc.resolve(o),t))}var Si=1,qt=2,ki=4,Ht=class extends St{constructor(e){super(e.doc),this.curSelectionFor=0,this.updated=0,this.meta=Object.create(null),this.time=Date.now(),this.curSelection=e.selection,this.storedMarks=e.storedMarks}get selection(){return this.curSelectionFor0}setStoredMarks(e){return this.storedMarks=e,this.updated|=qt,this}ensureMarks(e){return O.sameSet(this.storedMarks||this.selection.$from.marks(),e)||this.setStoredMarks(e),this}addStoredMark(e){return this.ensureMarks(e.addToSet(this.storedMarks||this.selection.$head.marks()))}removeStoredMark(e){return this.ensureMarks(e.removeFromSet(this.storedMarks||this.selection.$head.marks()))}get storedMarksSet(){return(this.updated&qt)>0}addStep(e,t){super.addStep(e,t),this.updated=this.updated&~qt,this.storedMarks=null}setTime(e){return this.time=e,this}replaceSelection(e){return this.selection.replace(this,e),this}replaceSelectionWith(e,t=!0){let n=this.selection;return t&&(e=e.mark(this.storedMarks||(n.empty?n.$from.marks():n.$from.marksAcross(n.$to)||O.none))),n.replaceWith(this,e),this}deleteSelection(){return this.selection.replace(this),this}insertText(e,t,n){let i=this.doc.type.schema;if(t==null)return e?this.replaceSelectionWith(i.text(e),!0):this.deleteSelection();{if(n==null&&(n=t),!e)return this.deleteRange(t,n);let s=this.storedMarks;if(!s){let o=this.doc.resolve(t);s=n==t?o.marks():o.marksAcross(this.doc.resolve(n))}return this.replaceRangeWith(t,n,i.text(e,s)),!this.selection.empty&&this.selection.to==t+e.length&&this.setSelection(k.near(this.selection.$to)),this}}setMeta(e,t){return this.meta[typeof e=="string"?e:e.key]=t,this}getMeta(e){return this.meta[typeof e=="string"?e:e.key]}get isGeneric(){for(let e in this.meta)return!1;return!0}scrollIntoView(){return this.updated|=ki,this}get scrolledIntoView(){return(this.updated&ki)>0}};function Ci(r,e){return!e||!r?r:r.bind(e)}var ve=class{constructor(e,t,n){this.name=e,this.init=Ci(t.init,n),this.apply=Ci(t.apply,n)}},bl=[new ve("doc",{init(r){return r.doc||r.schema.topNodeType.createAndFill()},apply(r){return r.doc}}),new ve("selection",{init(r,e){return r.selection||k.atStart(e.doc)},apply(r){return r.selection}}),new ve("storedMarks",{init(r){return r.storedMarks||null},apply(r,e,t,n){return n.selection.$cursor?r.storedMarks:null}}),new ve("scrollToSelection",{init(){return 0},apply(r,e){return r.scrolledIntoView?e+1:e}})],Ct=class{constructor(e,t){this.schema=e,this.plugins=[],this.pluginsByKey=Object.create(null),this.fields=bl.slice(),t&&t.forEach(n=>{if(this.pluginsByKey[n.key])throw new RangeError("Adding different instances of a keyed plugin ("+n.key+")");this.plugins.push(n),this.pluginsByKey[n.key]=n,n.spec.state&&this.fields.push(new ve(n.key,n.spec.state,n))})}},Rn=class r{constructor(e){this.config=e}get schema(){return this.config.schema}get plugins(){return this.config.plugins}apply(e){return this.applyTransaction(e).state}filterTransaction(e,t=-1){for(let n=0;nn.toJSON())),e&&typeof e=="object")for(let n in e){if(n=="doc"||n=="selection")throw new RangeError("The JSON fields `doc` and `selection` are reserved");let i=e[n],s=i.spec.state;s&&s.toJSON&&(t[n]=s.toJSON.call(i,this[i.key]))}return t}static fromJSON(e,t,n){if(!t)throw new RangeError("Invalid input for EditorState.fromJSON");if(!e.schema)throw new RangeError("Required config field 'schema' missing");let i=new Ct(e.schema,e.plugins),s=new r(i);return i.fields.forEach(o=>{if(o.name=="doc")s.doc=Q.fromJSON(e.schema,t.doc);else if(o.name=="selection")s.selection=k.fromJSON(s.doc,t.selection);else if(o.name=="storedMarks")t.storedMarks&&(s.storedMarks=t.storedMarks.map(e.schema.markFromJSON));else{if(n)for(let l in n){let a=n[l],f=a.spec.state;if(a.key==o.name&&f&&f.fromJSON&&Object.prototype.hasOwnProperty.call(t,l)){s[o.name]=f.fromJSON.call(a,e,t[l],s);return}}s[o.name]=o.init(e,s)}}),s}};function Mi(r,e,t){for(let n in r){let i=r[n];i instanceof Function?i=i.bind(e):n=="handleDOMEvents"&&(i=Mi(i,e,{})),t[n]=i}return t}var W=class{constructor(e){this.spec=e,this.props={},e.props&&Mi(e.props,this,this.props),this.key=e.key?e.key.key:Oi("plugin")}getState(e){return e[this.key]}},An=Object.create(null);function Oi(r){return r in An?r+"$"+ ++An[r]:(An[r]=0,r+"$")}var Oe=class{constructor(e="key"){this.key=Oi(e)}get(e){return e.config.pluginsByKey[this.key]}getState(e){return e[this.key]}};var Bs={};j(Bs,{Decoration:()=>te,DecorationSet:()=>J,EditorView:()=>nn,__endComposition:()=>ja,__parseFromClipboard:()=>Ua});var R=function(r){for(var e=0;;e++)if(r=r.previousSibling,!r)return e},ot=function(r){let e=r.assignedSlot||r.parentNode;return e&&e.nodeType==11?e.host:e},Vn=null,de=function(r,e,t){let n=Vn||(Vn=document.createRange());return n.setEnd(r,t??r.nodeValue.length),n.setStart(r,e||0),n},Sl=function(){Vn=null},He=function(r,e,t,n){return t&&(Ni(r,e,t,n,-1)||Ni(r,e,t,n,1))},kl=/^(img|br|input|textarea|hr)$/i;function Ni(r,e,t,n,i){for(var s;;){if(r==t&&e==n)return!0;if(e==(i<0?0:Y(r))){let o=r.parentNode;if(!o||o.nodeType!=1||At(r)||kl.test(r.nodeName)||r.contentEditable=="false")return!1;e=R(r)+(i<0?0:1),r=o}else if(r.nodeType==1){let o=r.childNodes[e+(i<0?-1:0)];if(o.nodeType==1&&o.contentEditable=="false")if(!((s=o.pmViewDesc)===null||s===void 0)&&s.ignoreForSelection)e+=i;else return!1;else r=o,e=i<0?Y(r):0}else return!1}}function Y(r){return r.nodeType==3?r.nodeValue.length:r.childNodes.length}function Cl(r,e){for(;;){if(r.nodeType==3&&e)return r;if(r.nodeType==1&&e>0){if(r.contentEditable=="false")return null;r=r.childNodes[e-1],e=Y(r)}else if(r.parentNode&&!At(r))e=R(r),r=r.parentNode;else return null}}function Ml(r,e){for(;;){if(r.nodeType==3&&e2),G=lt||(se?/Mac/.test(se.platform):!1),is=se?/Win/.test(se.platform):!1,pe=/Android \d/.test(Ee),It=!!Di&&"webkitFontSmoothing"in Di.documentElement.style,Dl=It?+(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent)||[0,0])[1]:0;function Tl(r){let e=r.defaultView&&r.defaultView.visualViewport;return e?{left:0,right:e.width,top:0,bottom:e.height}:{left:0,right:r.documentElement.clientWidth,top:0,bottom:r.documentElement.clientHeight}}function ue(r,e){return typeof r=="number"?r:r[e]}function El(r){let e=r.getBoundingClientRect(),t=e.width/r.offsetWidth||1,n=e.height/r.offsetHeight||1;return{left:e.left,right:e.left+r.clientWidth*t,top:e.top,bottom:e.top+r.clientHeight*n}}function Ti(r,e,t){if(!$n(e)&&e.left==0)return;let n=r.someProp("scrollThreshold")||0,i=r.someProp("scrollMargin")||5,s=r.dom.ownerDocument;for(let o=t||r.dom;o;){if(o.nodeType!=1){o=ot(o);continue}let l=o,a=l==s.body,f=a?Tl(s):El(l),c=0,h=0;if(e.topf.bottom-ue(n,"bottom")&&(h=e.bottom-e.top>f.bottom-f.top?e.top+ue(i,"top")-f.top:e.bottom-f.bottom+ue(i,"bottom")),e.leftf.right-ue(n,"right")&&(c=e.right-f.right+ue(i,"right")),c||h)if(a)s.defaultView.scrollBy(c,h);else{let d=l.scrollLeft,p=l.scrollTop;h&&(l.scrollTop+=h),c&&(l.scrollLeft+=c);let m=l.scrollLeft-d,g=l.scrollTop-p;e={left:e.left-m,top:e.top-g,right:e.right-m,bottom:e.bottom-g}}let u=a?"fixed":getComputedStyle(o).position;if(/^(fixed|sticky)$/.test(u))break;o=u=="absolute"?o.offsetParent:ot(o)}}function Al(r){let e=r.dom.getBoundingClientRect(),t=Math.max(0,e.top),n,i;for(let s=(e.left+e.right)/2,o=t+1;o=t-20){n=l,i=a.top;break}}return{refDOM:n,refTop:i,stack:ss(r.dom)}}function ss(r){let e=[],t=r.ownerDocument;for(let n=r;n&&(e.push({dom:n,top:n.scrollTop,left:n.scrollLeft}),r!=t);n=ot(n));return e}function Il({refDOM:r,refTop:e,stack:t}){let n=r?r.getBoundingClientRect().top:0;ls(t,n==0?0:n-e)}function ls(r,e){for(let t=0;t=l){o=Math.max(p.bottom,o),l=Math.min(p.top,l);let m=p.left>e.left?p.left-e.left:p.right=(p.left+p.right)/2?1:0));continue}}else p.top>e.top&&!a&&p.left<=e.left&&p.right>=e.left&&(a=c,f={left:Math.max(p.left,Math.min(p.right,e.left)),top:p.top});!t&&(e.left>=p.right&&e.top>=p.top||e.left>=p.left&&e.top>=p.bottom)&&(s=h+1)}}return!t&&a&&(t=a,i=f,n=0),t&&t.nodeType==3?Pl(t,i):!t||n&&t.nodeType==1?{node:r,offset:s}:as(t,i)}function Pl(r,e){let t=r.nodeValue.length,n=document.createRange(),i;for(let s=0;s=(o.left+o.right)/2?1:0)};break}}return n.detach(),i||{node:r,offset:0}}function ir(r,e){return r.left>=e.left-1&&r.left<=e.right+1&&r.top>=e.top-1&&r.top<=e.bottom+1}function Bl(r,e){let t=r.parentNode;return t&&/^li$/i.test(t.nodeName)&&e.left(o.left+o.right)/2?1:-1}return r.docView.posFromDOM(n,i,s)}function vl(r,e,t,n){let i=-1;for(let s=e,o=!1;s!=r.dom;){let l=r.docView.nearestDesc(s,!0),a;if(!l)return null;if(l.dom.nodeType==1&&(l.node.isBlock&&l.parent||!l.contentDOM)&&((a=l.dom.getBoundingClientRect()).width||a.height)&&(l.node.isBlock&&l.parent&&!/^T(R|BODY|HEAD|FOOT)$/.test(l.dom.nodeName)&&(!o&&a.left>n.left||a.top>n.top?i=l.posBefore:(!o&&a.right-1?i:r.docView.posFromDOM(e,t,-1)}function fs(r,e,t){let n=r.childNodes.length;if(n&&t.tope.top&&i++}let f;It&&i&&n.nodeType==1&&(f=n.childNodes[i-1]).nodeType==1&&f.contentEditable=="false"&&f.getBoundingClientRect().top>=e.top&&i--,n==r.dom&&i==n.childNodes.length-1&&n.lastChild.nodeType==1&&e.top>n.lastChild.getBoundingClientRect().bottom?l=r.state.doc.content.size:(i==0||n.nodeType!=1||n.childNodes[i-1].nodeName!="BR")&&(l=vl(r,n,i,e))}l==null&&(l=zl(r,o,e));let a=r.docView.nearestDesc(o,!0);return{pos:l,inside:a?a.posAtStart-a.border:-1}}function $n(r){return r.top=0&&i==n.nodeValue.length?(a--,c=1):t<0?a--:f++,Mt(we(de(n,a,f),c),c<0)}if(!r.state.doc.resolve(e-(s||0)).parent.inlineContent){if(s==null&&i&&(t<0||i==Y(n))){let a=n.childNodes[i-1];if(a.nodeType==1)return Pn(a.getBoundingClientRect(),!1)}if(s==null&&i=0)}if(s==null&&i&&(t<0||i==Y(n))){let a=n.childNodes[i-1],f=a.nodeType==3?de(a,Y(a)-(o?0:1)):a.nodeType==1&&(a.nodeName!="BR"||!a.nextSibling)?a:null;if(f)return Mt(we(f,1),!1)}if(s==null&&i=0)}function Mt(r,e){if(r.width==0)return r;let t=e?r.left:r.right;return{top:r.top,bottom:r.bottom,left:t,right:t}}function Pn(r,e){if(r.height==0)return r;let t=e?r.top:r.bottom;return{top:t,bottom:t,left:r.left,right:r.right}}function hs(r,e,t){let n=r.state,i=r.root.activeElement;n!=e&&r.updateState(e),i!=r.dom&&r.focus();try{return t()}finally{n!=e&&r.updateState(n),i!=r.dom&&i&&i.focus()}}function Ll(r,e,t){let n=e.selection,i=t=="up"?n.$from:n.$to;return hs(r,e,()=>{let{node:s}=r.docView.domFromPos(i.pos,t=="up"?-1:1);for(;;){let l=r.docView.nearestDesc(s,!0);if(!l)break;if(l.node.isBlock){s=l.contentDOM||l.dom;break}s=l.dom.parentNode}let o=cs(r,i.pos,1);for(let l=s.firstChild;l;l=l.nextSibling){let a;if(l.nodeType==1)a=l.getClientRects();else if(l.nodeType==3)a=de(l,0,l.nodeValue.length).getClientRects();else continue;for(let f=0;fc.top+1&&(t=="up"?o.top-c.top>(c.bottom-o.top)*2:c.bottom-o.bottom>(o.bottom-c.top)*2))return!1}}return!0})}var Wl=/[\u0590-\u08ac]/;function Jl(r,e,t){let{$head:n}=e.selection;if(!n.parent.isTextblock)return!1;let i=n.parentOffset,s=!i,o=i==n.parent.content.size,l=r.domSelection();return l?!Wl.test(n.parent.textContent)||!l.modify?t=="left"||t=="backward"?s:o:hs(r,e,()=>{let{focusNode:a,focusOffset:f,anchorNode:c,anchorOffset:h}=r.domSelectionRange(),u=l.caretBidiLevel;l.modify("move",t,"character");let d=n.depth?r.docView.domAfterPos(n.before()):r.dom,{focusNode:p,focusOffset:m}=r.domSelectionRange(),g=p&&!d.contains(p.nodeType==1?p:p.parentNode)||a==p&&f==m;try{l.collapse(c,h),a&&(a!=c||f!=h)&&l.extend&&l.extend(a,f)}catch{}return u!=null&&(l.caretBidiLevel=u),g}):n.pos==n.start()||n.pos==n.end()}var Ei=null,Ai=null,Ii=!1;function $l(r,e,t){return Ei==e&&Ai==t?Ii:(Ei=e,Ai=t,Ii=t=="up"||t=="down"?Ll(r,e,t):Jl(r,e,t))}var _=0,Ri=1,Le=2,ee=3,Ue=class{constructor(e,t,n,i){this.parent=e,this.children=t,this.dom=n,this.contentDOM=i,this.dirty=_,n.pmViewDesc=this}matchesWidget(e){return!1}matchesMark(e){return!1}matchesNode(e,t,n){return!1}matchesHack(e){return!1}parseRule(e){return null}stopEvent(e){return!1}get size(){let e=0;for(let t=0;tR(this.contentDOM);else if(this.contentDOM&&this.contentDOM!=this.dom&&this.dom.contains(this.contentDOM))i=e.compareDocumentPosition(this.contentDOM)&2;else if(this.dom.firstChild){if(t==0)for(let s=e;;s=s.parentNode){if(s==this.dom){i=!1;break}if(s.previousSibling)break}if(i==null&&t==e.childNodes.length)for(let s=e;;s=s.parentNode){if(s==this.dom){i=!0;break}if(s.nextSibling)break}}return i??n>0?this.posAtEnd:this.posAtStart}nearestDesc(e,t=!1){for(let n=!0,i=e;i;i=i.parentNode){let s=this.getDesc(i),o;if(s&&(!t||s.node))if(n&&(o=s.nodeDOM)&&!(o.nodeType==1?o.contains(e.nodeType==1?e:e.parentNode):o==e))n=!1;else return s}}getDesc(e){let t=e.pmViewDesc;for(let n=t;n;n=n.parent)if(n==this)return t}posFromDOM(e,t,n){for(let i=e;i;i=i.parentNode){let s=this.getDesc(i);if(s)return s.localPosFromDOM(e,t,n)}return-1}descAt(e){for(let t=0,n=0;te||o instanceof Gt){i=e-s;break}s=l}if(i)return this.children[n].domFromPos(i-this.children[n].border,t);for(let s;n&&!(s=this.children[n-1]).size&&s instanceof Ut&&s.side>=0;n--);if(t<=0){let s,o=!0;for(;s=n?this.children[n-1]:null,!(!s||s.dom.parentNode==this.contentDOM);n--,o=!1);return s&&t&&o&&!s.border&&!s.domAtom?s.domFromPos(s.size,t):{node:this.contentDOM,offset:s?R(s.dom)+1:0}}else{let s,o=!0;for(;s=n=c&&t<=f-a.border&&a.node&&a.contentDOM&&this.contentDOM.contains(a.contentDOM))return a.parseRange(e,t,c);e=o;for(let h=l;h>0;h--){let u=this.children[h-1];if(u.size&&u.dom.parentNode==this.contentDOM&&!u.emptyChildAt(1)){i=R(u.dom)+1;break}e-=u.size}i==-1&&(i=0)}if(i>-1&&(f>t||l==this.children.length-1)){t=f;for(let c=l+1;cp&&ot){let p=l;l=a,a=p}let d=document.createRange();d.setEnd(a.node,a.offset),d.setStart(l.node,l.offset),f.removeAllRanges(),f.addRange(d)}}ignoreMutation(e){return!this.contentDOM&&e.type!="selection"}get contentLost(){return this.contentDOM&&this.contentDOM!=this.dom&&!this.dom.contains(this.contentDOM)}markDirty(e,t){for(let n=0,i=0;i=n:en){let l=n+s.border,a=o-s.border;if(e>=l&&t<=a){this.dirty=e==n||t==o?Le:Ri,e==l&&t==a&&(s.contentLost||s.dom.parentNode!=this.contentDOM)?s.dirty=ee:s.markDirty(e-l,t-l);return}else s.dirty=s.dom==s.contentDOM&&s.dom.parentNode==this.contentDOM&&!s.children.length?Le:ee}n=o}this.dirty=Le}markParentsDirty(){let e=1;for(let t=this.parent;t;t=t.parent,e++){let n=e==1?Le:Ri;t.dirty{if(!s)return i;if(s.parent)return s.parent.posBeforeChild(s)})),!t.type.spec.raw){if(o.nodeType!=1){let l=document.createElement("span");l.appendChild(o),o=l}o.contentEditable="false",o.classList.add("ProseMirror-widget")}super(e,[],o,null),this.widget=t,this.widget=t,s=this}matchesWidget(e){return this.dirty==_&&e.type.eq(this.widget.type)}parseRule(){return{ignore:!0}}stopEvent(e){let t=this.widget.spec.stopEvent;return t?t(e):!1}ignoreMutation(e){return e.type!="selection"||this.widget.spec.ignoreSelection}destroy(){this.widget.type.destroy(this.dom),super.destroy()}get domAtom(){return!0}get ignoreForSelection(){return!!this.widget.type.spec.relaxedSide}get side(){return this.widget.type.side}},qn=class extends Ue{constructor(e,t,n,i){super(e,[],t,null),this.textDOM=n,this.text=i}get size(){return this.text.length}localPosFromDOM(e,t){return e!=this.textDOM?this.posAtStart+(t?this.size:0):this.posAtStart+t}domFromPos(e){return{node:this.textDOM,offset:e}}ignoreMutation(e){return e.type==="characterData"&&e.target.nodeValue==e.oldValue}},Je=class r extends Ue{constructor(e,t,n,i,s){super(e,[],n,i),this.mark=t,this.spec=s}static create(e,t,n,i){let s=i.nodeViews[t.type.name],o=s&&s(t,i,n);return(!o||!o.dom)&&(o=ke.renderSpec(document,t.type.spec.toDOM(t,n),null,t.attrs)),new r(e,t,o.dom,o.contentDOM||o.dom,o)}parseRule(){return this.dirty&ee||this.mark.type.spec.reparseInView?null:{mark:this.mark.type.name,attrs:this.mark.attrs,contentElement:this.contentDOM}}matchesMark(e){return this.dirty!=ee&&this.mark.eq(e)}markDirty(e,t){if(super.markDirty(e,t),this.dirty!=_){let n=this.parent;for(;!n.node;)n=n.parent;n.dirty0&&(s=jn(s,0,e,n));for(let l=0;l{if(!a)return o;if(a.parent)return a.parent.posBeforeChild(a)},n,i),c=f&&f.dom,h=f&&f.contentDOM;if(t.isText){if(!c)c=document.createTextNode(t.text);else if(c.nodeType!=3)throw new RangeError("Text must be rendered as a DOM text node")}else c||({dom:c,contentDOM:h}=ke.renderSpec(document,t.type.spec.toDOM(t),null,t.attrs));!h&&!t.isText&&c.nodeName!="BR"&&(c.hasAttribute("contenteditable")||(c.contentEditable="false"),t.type.spec.draggable&&(c.draggable=!0));let u=c;return c=ps(c,n,t),f?a=new Kn(e,t,n,i,c,h||null,u,f):t.isText?new jt(e,t,n,i,c,u):new r(e,t,n,i,c,h||null,u)}parseRule(e){if(this.node.type.spec.reparseInView)return null;let t={node:this.node.type.name,attrs:this.node.attrs};if(this.node.type.whitespace=="pre"&&(t.preserveWhitespace="full"),!this.contentDOM)t.getContent=()=>this.node.content;else if(!this.contentLost)t.contentElement=this.contentDOM;else{for(let n=this.children.length-1;n>=0;n--){let i=this.children[n];if(this.dom.contains(i.dom.parentNode)){t.contentElement=i.dom.parentNode;break}}if(!t.contentElement){let n=e&&e.find(i=>i.nodeType==1&&e.indexOf(i.parentNode)<0&&this.dom.contains(i));n?t.contentElement=n:t.getContent=()=>y.empty}}return t}matchesNode(e,t,n){return this.dirty==_&&e.eq(this.node)&&Yt(t,this.outerDeco)&&n.eq(this.innerDeco)}get size(){return this.node.nodeSize}get border(){return this.node.isLeaf?0:1}updateChildren(e,t){let n=this.node.inlineContent,i=t,s=e.composing?this.localCompositionInfo(e,t):null,o=s&&s.pos>-1?s:null,l=s&&s.pos<0,a=new Un(this,o&&o.node,e);Ul(this.node,this.innerDeco,(f,c,h)=>{f.spec.marks?a.syncToMarks(f.spec.marks,n,e,c):f.type.side>=0&&!h&&a.syncToMarks(c==this.node.childCount?O.none:this.node.child(c).marks,n,e,c),a.placeWidget(f,e,i)},(f,c,h,u)=>{a.syncToMarks(f.marks,n,e,u);let d;a.findNodeMatch(f,c,h,u)||l&&e.state.selection.from>i&&e.state.selection.to-1&&a.updateNodeAt(f,c,h,d,e)||a.updateNextNode(f,c,h,e,u,i)||a.addNode(f,c,h,e,i),i+=f.nodeSize}),a.syncToMarks([],n,e,0),this.node.isTextblock&&a.addTextblockHacks(),a.destroyRest(),(a.changed||this.dirty==Le)&&(o&&this.protectLocalComposition(e,o),us(this.contentDOM,this.children,e),lt&&jl(this.dom))}localCompositionInfo(e,t){let{from:n,to:i}=e.state.selection;if(!(e.state.selection instanceof C)||nt+this.node.content.size)return null;let s=e.input.compositionNode;if(!s||!this.dom.contains(s.parentNode))return null;if(this.node.inlineContent){let o=s.nodeValue,l=Gl(this.node.content,o,n-t,i-t);return l<0?null:{node:s,pos:l,text:o}}else return{node:s,pos:-1,text:""}}protectLocalComposition(e,{node:t,pos:n,text:i}){if(this.getDesc(t))return;let s=t;for(;s.parentNode!=this.contentDOM;s=s.parentNode){for(;s.previousSibling;)s.parentNode.removeChild(s.previousSibling);for(;s.nextSibling;)s.parentNode.removeChild(s.nextSibling);s.pmViewDesc&&(s.pmViewDesc=void 0)}let o=new qn(this,s,t,i);e.input.compositionNodes.push(o),this.children=jn(this.children,n,n+i.length,e,o)}update(e,t,n,i){return this.dirty==ee||!e.sameMarkup(this.node)?!1:(this.updateInner(e,t,n,i),!0)}updateInner(e,t,n,i){this.updateOuterDeco(t),this.node=e,this.innerDeco=n,this.contentDOM&&this.updateChildren(i,this.posAtStart),this.dirty=_}updateOuterDeco(e){if(Yt(e,this.outerDeco))return;let t=this.nodeDOM.nodeType!=1,n=this.dom;this.dom=ds(this.dom,this.nodeDOM,Hn(this.outerDeco,this.node,t),Hn(e,this.node,t)),this.dom!=n&&(n.pmViewDesc=void 0,this.dom.pmViewDesc=this),this.outerDeco=e}selectNode(){this.nodeDOM.nodeType==1&&(this.nodeDOM.classList.add("ProseMirror-selectednode"),(this.contentDOM||!this.node.type.spec.draggable)&&(this.nodeDOM.draggable=!0))}deselectNode(){this.nodeDOM.nodeType==1&&(this.nodeDOM.classList.remove("ProseMirror-selectednode"),(this.contentDOM||!this.node.type.spec.draggable)&&this.nodeDOM.removeAttribute("draggable"))}get domAtom(){return this.node.isAtom}};function Pi(r,e,t,n,i){ps(n,e,r);let s=new Te(void 0,r,e,t,n,n,n);return s.contentDOM&&s.updateChildren(i,0),s}var jt=class r extends Te{constructor(e,t,n,i,s,o){super(e,t,n,i,s,null,o)}parseRule(){let e=this.nodeDOM.parentNode;for(;e&&e!=this.dom&&!e.pmIsDeco;)e=e.parentNode;return{skip:e||!0}}update(e,t,n,i){return this.dirty==ee||this.dirty!=_&&!this.inParent()||!e.sameMarkup(this.node)?!1:(this.updateOuterDeco(t),(this.dirty!=_||e.text!=this.node.text)&&e.text!=this.nodeDOM.nodeValue&&(this.nodeDOM.nodeValue=e.text,i.trackWrites==this.nodeDOM&&(i.trackWrites=null)),this.node=e,this.dirty=_,!0)}inParent(){let e=this.parent.contentDOM;for(let t=this.nodeDOM;t;t=t.parentNode)if(t==e)return!0;return!1}domFromPos(e){return{node:this.nodeDOM,offset:e}}localPosFromDOM(e,t,n){return e==this.nodeDOM?this.posAtStart+Math.min(t,this.node.text.length):super.localPosFromDOM(e,t,n)}ignoreMutation(e){return e.type!="characterData"&&e.type!="selection"}slice(e,t,n){let i=this.node.cut(e,t),s=document.createTextNode(i.text);return new r(this.parent,i,this.outerDeco,this.innerDeco,s,s)}markDirty(e,t){super.markDirty(e,t),this.dom!=this.nodeDOM&&(e==0||t==this.nodeDOM.nodeValue.length)&&(this.dirty=ee)}get domAtom(){return!1}isText(e){return this.node.text==e}},Gt=class extends Ue{parseRule(){return{ignore:!0}}matchesHack(e){return this.dirty==_&&this.dom.nodeName==e}get domAtom(){return!0}get ignoreForCoords(){return this.dom.nodeName=="IMG"}},Kn=class extends Te{constructor(e,t,n,i,s,o,l,a){super(e,t,n,i,s,o,l),this.spec=a}update(e,t,n,i){if(this.dirty==ee)return!1;if(this.spec.update&&(this.node.type==e.type||this.spec.multiType)){let s=this.spec.update(e,t,n);return s&&this.updateInner(e,t,n,i),s}else return!this.contentDOM&&!e.isLeaf?!1:super.update(e,t,n,i)}selectNode(){this.spec.selectNode?this.spec.selectNode():super.selectNode()}deselectNode(){this.spec.deselectNode?this.spec.deselectNode():super.deselectNode()}setSelection(e,t,n,i){this.spec.setSelection?this.spec.setSelection(e,t,n.root):super.setSelection(e,t,n,i)}destroy(){this.spec.destroy&&this.spec.destroy(),super.destroy()}stopEvent(e){return this.spec.stopEvent?this.spec.stopEvent(e):!1}ignoreMutation(e){return this.spec.ignoreMutation?this.spec.ignoreMutation(e):super.ignoreMutation(e)}};function us(r,e,t){let n=r.firstChild,i=!1;for(let s=0;s>1,l=Math.min(o,e.length);for(;s-1)a>this.index&&(this.changed=!0,this.destroyBetween(this.index,a)),this.top=this.top.children[this.index];else{let c=Je.create(this.top,e[o],t,n);this.top.children.splice(this.index,0,c),this.top=c,this.changed=!0}this.index=0,o++}}findNodeMatch(e,t,n,i){let s=-1,o;if(i>=this.preMatch.index&&(o=this.preMatch.matches[i-this.preMatch.index]).parent==this.top&&o.matchesNode(e,t,n))s=this.top.children.indexOf(o,this.index);else for(let l=this.index,a=Math.min(this.top.children.length,l+5);l0;){let l;for(;;)if(n){let f=t.children[n-1];if(f instanceof Je)t=f,n=f.children.length;else{l=f,n--;break}}else{if(t==e)break e;n=t.parent.children.indexOf(t),t=t.parent}let a=l.node;if(a){if(a!=r.child(i-1))break;--i,s.set(l,i),o.push(l)}}return{index:i,matched:s,matches:o.reverse()}}function Hl(r,e){return r.type.side-e.type.side}function Ul(r,e,t,n){let i=e.locals(r),s=0;if(i.length==0){for(let f=0;fs;)l.push(i[o++]);let p=s+u.nodeSize;if(u.isText){let g=p;o!g.inline):l.slice();n(u,m,e.forChild(s,u),d),s=p}}function jl(r){if(r.nodeName=="UL"||r.nodeName=="OL"){let e=r.style.cssText;r.style.cssText=e+"; list-style: square !important",window.getComputedStyle(r).listStyle,r.style.cssText=e}}function Gl(r,e,t,n){for(let i=0,s=0;i=t){if(s>=n&&a.slice(n-e.length-l,n-l)==e)return n-e.length;let f=l=0&&f+e.length+l>=t)return l+f;if(t==n&&a.length>=n+e.length-l&&a.slice(n-l,n-l+e.length)==e)return n}}return-1}function jn(r,e,t,n,i){let s=[];for(let o=0,l=0;o=t||c<=e?s.push(a):(ft&&s.push(a.slice(t-f,a.size,n)))}return s}function sr(r,e=null){let t=r.domSelectionRange(),n=r.state.doc;if(!t.focusNode)return null;let i=r.docView.nearestDesc(t.focusNode),s=i&&i.size==0,o=r.docView.posFromDOM(t.focusNode,t.focusOffset,1);if(o<0)return null;let l=n.resolve(o),a,f;if(rn(t)){for(a=o;i&&!i.node;)i=i.parent;let h=i.node;if(i&&h.isAtom&&S.isSelectable(h)&&i.parent&&!(h.isInline&&Ol(t.focusNode,t.focusOffset,i.dom))){let u=i.posBefore;f=new S(o==u?l:n.resolve(u))}}else{if(t instanceof r.dom.ownerDocument.defaultView.Selection&&t.rangeCount>1){let h=o,u=o;for(let d=0;d{(t.anchorNode!=n||t.anchorOffset!=i)&&(e.removeEventListener("selectionchange",r.input.hideSelectionGuard),setTimeout(()=>{(!ms(r)||r.state.selection.visible)&&r.dom.classList.remove("ProseMirror-hideselection")},20))})}function Xl(r){let e=r.domSelection();if(!e)return;let t=r.cursorWrapper.dom,n=t.nodeName=="IMG";n?e.collapse(t.parentNode,R(t)+1):e.collapse(t,0),!n&&!r.state.selection.visible&&U&&De<=11&&(t.disabled=!0,t.disabled=!1)}function gs(r,e){if(e instanceof S){let t=r.docView.descAt(e.from);t!=r.lastSelectedViewDesc&&(Vi(r),t&&t.selectNode(),r.lastSelectedViewDesc=t)}else Vi(r)}function Vi(r){r.lastSelectedViewDesc&&(r.lastSelectedViewDesc.parent&&r.lastSelectedViewDesc.deselectNode(),r.lastSelectedViewDesc=void 0)}function or(r,e,t,n){return r.someProp("createSelectionBetween",i=>i(r,e,t))||C.between(e,t,n)}function Li(r){return r.editable&&!r.hasFocus()?!1:ys(r)}function ys(r){let e=r.domSelectionRange();if(!e.anchorNode)return!1;try{return r.dom.contains(e.anchorNode.nodeType==3?e.anchorNode.parentNode:e.anchorNode)&&(r.editable||r.dom.contains(e.focusNode.nodeType==3?e.focusNode.parentNode:e.focusNode))}catch{return!1}}function _l(r){let e=r.docView.domFromPos(r.state.selection.anchor,0),t=r.domSelectionRange();return He(e.node,e.offset,t.anchorNode,t.anchorOffset)}function Gn(r,e){let{$anchor:t,$head:n}=r.selection,i=e>0?t.max(n):t.min(n),s=i.parent.inlineContent?i.depth?r.doc.resolve(e>0?i.after():i.before()):null:i;return s&&k.findFrom(s,e)}function Ne(r,e){return r.dispatch(r.state.tr.setSelection(e).scrollIntoView()),!0}function Wi(r,e,t){let n=r.state.selection;if(n instanceof C)if(t.indexOf("s")>-1){let{$head:i}=n,s=i.textOffset?null:e<0?i.nodeBefore:i.nodeAfter;if(!s||s.isText||!s.isLeaf)return!1;let o=r.state.doc.resolve(i.pos+s.nodeSize*(e<0?-1:1));return Ne(r,new C(n.$anchor,o))}else if(n.empty){if(r.endOfTextblock(e>0?"forward":"backward")){let i=Gn(r.state,e);return i&&i instanceof S?Ne(r,i):!1}else if(!(G&&t.indexOf("m")>-1)){let i=n.$head,s=i.textOffset?null:e<0?i.nodeBefore:i.nodeAfter,o;if(!s||s.isText)return!1;let l=e<0?i.pos-s.nodeSize:i.pos;return s.isAtom||(o=r.docView.descAt(l))&&!o.contentDOM?S.isSelectable(s)?Ne(r,new S(e<0?r.state.doc.resolve(i.pos-s.nodeSize):i)):It?Ne(r,new C(r.state.doc.resolve(e<0?l:l+s.nodeSize))):!1:!1}}else return!1;else{if(n instanceof S&&n.node.isInline)return Ne(r,new C(e>0?n.$to:n.$from));{let i=Gn(r.state,e);return i?Ne(r,i):!1}}}function Xt(r){return r.nodeType==3?r.nodeValue.length:r.childNodes.length}function wt(r,e){let t=r.pmViewDesc;return t&&t.size==0&&(e<0||r.nextSibling||r.nodeName!="BR")}function it(r,e){return e<0?Ql(r):Zl(r)}function Ql(r){let e=r.domSelectionRange(),t=e.focusNode,n=e.focusOffset;if(!t)return;let i,s,o=!1;for(X&&t.nodeType==1&&n0){if(t.nodeType!=1)break;{let l=t.childNodes[n-1];if(wt(l,-1))i=t,s=--n;else if(l.nodeType==3)t=l,n=t.nodeValue.length;else break}}else{if(xs(t))break;{let l=t.previousSibling;for(;l&&wt(l,-1);)i=t.parentNode,s=R(l),l=l.previousSibling;if(l)t=l,n=Xt(t);else{if(t=t.parentNode,t==r.dom)break;n=0}}}o?Yn(r,t,n):i&&Yn(r,i,s)}function Zl(r){let e=r.domSelectionRange(),t=e.focusNode,n=e.focusOffset;if(!t)return;let i=Xt(t),s,o;for(;;)if(n{r.state==i&&ge(r)},50)}function Ji(r,e){let t=r.state.doc.resolve(e);if(!(P||is)&&t.parent.inlineContent){let i=r.coordsAtPos(e);if(e>t.start()){let s=r.coordsAtPos(e-1),o=(s.top+s.bottom)/2;if(o>i.top&&o1)return s.lefti.top&&o1)return s.left>i.left?"ltr":"rtl"}}return getComputedStyle(r.dom).direction=="rtl"?"rtl":"ltr"}function $i(r,e,t){let n=r.state.selection;if(n instanceof C&&!n.empty||t.indexOf("s")>-1||G&&t.indexOf("m")>-1)return!1;let{$from:i,$to:s}=n;if(!i.parent.inlineContent||r.endOfTextblock(e<0?"up":"down")){let o=Gn(r.state,e);if(o&&o instanceof S)return Ne(r,o)}if(!i.parent.inlineContent){let o=e<0?i:s,l=n instanceof H?k.near(o,e):k.findFrom(o,e);return l?Ne(r,l):!1}return!1}function qi(r,e){if(!(r.state.selection instanceof C))return!0;let{$head:t,$anchor:n,empty:i}=r.state.selection;if(!t.sameParent(n))return!0;if(!i)return!1;if(r.endOfTextblock(e>0?"forward":"backward"))return!0;let s=!t.textOffset&&(e<0?t.nodeBefore:t.nodeAfter);if(s&&!s.isText){let o=r.state.tr;return e<0?o.delete(t.pos-s.nodeSize,t.pos):o.delete(t.pos,t.pos+s.nodeSize),r.dispatch(o),!0}return!1}function Ki(r,e,t){r.domObserver.stop(),e.contentEditable=t,r.domObserver.start()}function na(r){if(!L||r.state.selection.$head.parentOffset>0)return!1;let{focusNode:e,focusOffset:t}=r.domSelectionRange();if(e&&e.nodeType==1&&t==0&&e.firstChild&&e.firstChild.contentEditable=="false"){let n=e.firstChild;Ki(r,n,"true"),setTimeout(()=>Ki(r,n,"false"),20)}return!1}function ra(r){let e="";return r.ctrlKey&&(e+="c"),r.metaKey&&(e+="m"),r.altKey&&(e+="a"),r.shiftKey&&(e+="s"),e}function ia(r,e){let t=e.keyCode,n=ra(e);if(t==8||G&&t==72&&n=="c")return qi(r,-1)||it(r,-1);if(t==46&&!e.shiftKey||G&&t==68&&n=="c")return qi(r,1)||it(r,1);if(t==13||t==27)return!0;if(t==37||G&&t==66&&n=="c"){let i=t==37?Ji(r,r.state.selection.from)=="ltr"?-1:1:-1;return Wi(r,i,n)||it(r,i)}else if(t==39||G&&t==70&&n=="c"){let i=t==39?Ji(r,r.state.selection.from)=="ltr"?1:-1:1;return Wi(r,i,n)||it(r,i)}else{if(t==38||G&&t==80&&n=="c")return $i(r,-1,n)||it(r,-1);if(t==40||G&&t==78&&n=="c")return na(r)||$i(r,1,n)||it(r,1);if(n==(G?"m":"c")&&(t==66||t==73||t==89||t==90))return!0}return!1}function lr(r,e){r.someProp("transformCopied",d=>{e=d(e,r)});let t=[],{content:n,openStart:i,openEnd:s}=e;for(;i>1&&s>1&&n.childCount==1&&n.firstChild.childCount==1;){i--,s--;let d=n.firstChild;t.push(d.type.name,d.attrs!=d.type.defaultAttrs?d.attrs:null),n=d.content}let o=r.someProp("clipboardSerializer")||ke.fromSchema(r.state.schema),l=Ms(),a=l.createElement("div");a.appendChild(o.serializeFragment(n,{document:l}));let f=a.firstChild,c,h=0;for(;f&&f.nodeType==1&&(c=Cs[f.nodeName.toLowerCase()]);){for(let d=c.length-1;d>=0;d--){let p=l.createElement(c[d]);for(;a.firstChild;)p.appendChild(a.firstChild);a.appendChild(p),h++}f=a.firstChild}f&&f.nodeType==1&&f.setAttribute("data-pm-slice",`${i} ${s}${h?` -${h}`:""} ${JSON.stringify(t)}`);let u=r.someProp("clipboardTextSerializer",d=>d(e,r))||e.content.textBetween(0,e.content.size,` + +`);return{dom:a,text:u,slice:e}}function ar(r,e,t,n,i){let s=i.parent.type.spec.code,o,l;if(!t&&!e)return null;let a=!!e&&(n||s||!t);if(a){if(r.someProp("transformPastedText",u=>{e=u(e,s||n,r)}),s)return l=new x(y.from(r.state.schema.text(e.replace(/\r\n?/g,` +`))),0,0),r.someProp("transformPasted",u=>{l=u(l,r,!0)}),l;let h=r.someProp("clipboardTextParser",u=>u(e,i,n,r));if(h)l=h;else{let u=i.marks(),{schema:d}=r.state,p=ke.fromSchema(d);o=document.createElement("div"),e.split(/(?:\r\n?|\n)+/).forEach(m=>{let g=o.appendChild(document.createElement("p"));m&&g.appendChild(p.serializeNode(d.text(m,u)))})}}else r.someProp("transformPastedHTML",h=>{t=h(t,r)}),o=aa(t),It&&fa(o);let f=o&&o.querySelector("[data-pm-slice]"),c=f&&/^(\d+) (\d+)(?: -(\d+))? (.*)/.exec(f.getAttribute("data-pm-slice")||"");if(c&&c[3])for(let h=+c[3];h>0;h--){let u=o.firstChild;for(;u&&u.nodeType!=1;)u=u.nextSibling;if(!u)break;o=u}if(l||(l=(r.someProp("clipboardParser")||r.someProp("domParser")||Xe.fromSchema(r.state.schema)).parseSlice(o,{preserveWhitespace:!!(a||c),context:i,ruleFromNode(u){return u.nodeName=="BR"&&!u.nextSibling&&u.parentNode&&!sa.test(u.parentNode.nodeName)?{ignore:!0}:null}})),c)l=ca(Hi(l,+c[1],+c[2]),c[4]);else if(l=x.maxOpen(oa(l.content,i),!0),l.openStart||l.openEnd){let h=0,u=0;for(let d=l.content.firstChild;h{l=h(l,r,a)}),l}var sa=/^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/i;function oa(r,e){if(r.childCount<2)return r;for(let t=e.depth;t>=0;t--){let i=e.node(t).contentMatchAt(e.index(t)),s,o=[];if(r.forEach(l=>{if(!o)return;let a=i.findWrapping(l.type),f;if(!a)return o=null;if(f=o.length&&s.length&&Ss(a,s,l,o[o.length-1],0))o[o.length-1]=f;else{o.length&&(o[o.length-1]=ks(o[o.length-1],s.length));let c=bs(l,a);o.push(c),i=i.matchType(c.type),s=a}}),o)return y.from(o)}return r}function bs(r,e,t=0){for(let n=e.length-1;n>=t;n--)r=e[n].create(null,y.from(r));return r}function Ss(r,e,t,n,i){if(i1&&(s=0),i=t&&(l=e<0?o.contentMatchAt(0).fillBefore(l,s<=i).append(l):l.append(o.contentMatchAt(o.childCount).fillBefore(y.empty,!0))),r.replaceChild(e<0?0:r.childCount-1,o.copy(l))}function Hi(r,e,t){return et})),zn.createHTML(r)):r}function aa(r){let e=/^(\s*]*>)*/.exec(r);e&&(r=r.slice(e[0].length));let t=Ms(),n=t.body,i=/<([a-z][^>\s]+)/i.exec(r),s;if((s=i&&Cs[i[1].toLowerCase()])&&(r=s.map(o=>"<"+o+">").join("")+r+s.map(o=>"").reverse().join("")),n.innerHTML=la(r),s)for(let o=0;o=0;l-=2){let a=t.nodes[n[l]];if(!a||a.hasRequiredAttrs())break;i=y.from(a.create(n[l+1],i)),s++,o++}return new x(i,s,o)}var $={},q={},ha={touchstart:!0,touchmove:!0},_n=class{constructor(){this.shiftKey=!1,this.mouseDown=null,this.lastKeyCode=null,this.lastKeyCodeTime=0,this.lastClick={time:0,x:0,y:0,type:"",button:0},this.lastSelectionOrigin=null,this.lastSelectionTime=0,this.lastIOSEnter=0,this.lastIOSEnterFallbackTimeout=-1,this.lastFocus=0,this.lastTouch=0,this.lastChromeDelete=0,this.composing=!1,this.compositionNode=null,this.composingTimeout=-1,this.compositionNodes=[],this.compositionEndedAt=-2e8,this.compositionID=1,this.badSafariComposition=!1,this.compositionPendingChanges=0,this.domChangeCount=0,this.eventHandlers=Object.create(null),this.hideSelectionGuard=null}};function ua(r){for(let e in $){let t=$[e];r.dom.addEventListener(e,r.input.eventHandlers[e]=n=>{pa(r,n)&&!fr(r,n)&&(r.editable||!(n.type in q))&&t(r,n)},ha[e]?{passive:!0}:void 0)}L&&r.dom.addEventListener("input",()=>null),Qn(r)}function me(r,e){r.input.lastSelectionOrigin=e,r.input.lastSelectionTime=Date.now()}function da(r){r.input.mouseDown&&r.input.mouseDown.done(),r.domObserver.stop();for(let e in r.input.eventHandlers)r.dom.removeEventListener(e,r.input.eventHandlers[e]);clearTimeout(r.input.composingTimeout),clearTimeout(r.input.lastIOSEnterFallbackTimeout)}function Qn(r){r.someProp("handleDOMEvents",e=>{for(let t in e)r.input.eventHandlers[t]||r.dom.addEventListener(t,r.input.eventHandlers[t]=n=>fr(r,n))})}function fr(r,e){return r.someProp("handleDOMEvents",t=>{let n=t[e.type];return n?n(r,e)||e.defaultPrevented:!1})}function pa(r,e){if(!e.bubbles)return!0;if(e.defaultPrevented)return!1;for(let t=e.target;t!=r.dom;t=t.parentNode)if(!t||t.nodeType==11||t.pmViewDesc&&t.pmViewDesc.stopEvent(e))return!1;return!0}function ma(r,e){!fr(r,e)&&$[e.type]&&(r.editable||!(e.type in q))&&$[e.type](r,e)}q.keydown=(r,e)=>{let t=e;if(r.input.shiftKey=t.keyCode==16||t.shiftKey,!Ns(r)&&(r.input.lastKeyCode=t.keyCode,r.input.lastKeyCodeTime=Date.now(),!(pe&&P&&t.keyCode==13)))if(t.keyCode!=229&&r.domObserver.forceFlush(),lt&&t.keyCode==13&&!t.ctrlKey&&!t.altKey&&!t.metaKey){let n=Date.now();r.input.lastIOSEnter=n,r.input.lastIOSEnterFallbackTimeout=setTimeout(()=>{r.input.lastIOSEnter==n&&(r.someProp("handleKeyDown",i=>i(r,Ve(13,"Enter"))),r.input.lastIOSEnter=0)},200)}else r.someProp("handleKeyDown",n=>n(r,t))||ia(r,t)?t.preventDefault():me(r,"key")};q.keyup=(r,e)=>{e.keyCode==16&&(r.input.shiftKey=!1)};q.keypress=(r,e)=>{let t=e;if(Ns(r)||!t.charCode||t.ctrlKey&&!t.altKey||G&&t.metaKey)return;if(r.someProp("handleKeyPress",i=>i(r,t))){t.preventDefault();return}let n=r.state.selection;if(!(n instanceof C)||!n.$from.sameParent(n.$to)){let i=String.fromCharCode(t.charCode),s=()=>r.state.tr.insertText(i).scrollIntoView();!/[\r\n]/.test(i)&&!r.someProp("handleTextInput",o=>o(r,n.$from.pos,n.$to.pos,i,s))&&r.dispatch(s()),t.preventDefault()}};function Rt(r){return{left:r.clientX,top:r.clientY}}function ga(r,e){let t=e.x-r.clientX,n=e.y-r.clientY;return t*t+n*n<100}function cr(r,e,t,n,i){if(n==-1)return!1;let s=r.state.doc.resolve(n);for(let o=s.depth+1;o>0;o--)if(r.someProp(e,l=>o>s.depth?l(r,t,s.nodeAfter,s.before(o),i,!0):l(r,t,s.node(o),s.before(o),i,!1)))return!0;return!1}function Pt(r,e,t){if(r.focused||r.focus(),r.state.selection.eq(e))return;let n=r.state.tr.setSelection(e);t=="pointer"&&n.setMeta("pointer",!0),r.dispatch(n)}function ya(r,e){if(e==-1)return!1;let t=r.state.doc.resolve(e),n=t.nodeAfter;return n&&n.isAtom&&S.isSelectable(n)?(Pt(r,new S(t),"pointer"),!0):!1}function xa(r,e){if(e==-1)return!1;let t=r.state.selection,n,i;t instanceof S&&(n=t.node);let s=r.state.doc.resolve(e);for(let o=s.depth+1;o>0;o--){let l=o>s.depth?s.nodeAfter:s.node(o);if(S.isSelectable(l)){n&&t.$from.depth>0&&o>=t.$from.depth&&s.before(t.$from.depth+1)==t.$from.pos?i=s.before(t.$from.depth):i=s.before(o);break}}return i!=null?(Pt(r,S.create(r.state.doc,i),"pointer"),!0):!1}function ba(r,e,t,n,i){return cr(r,"handleClickOn",e,t,n)||r.someProp("handleClick",s=>s(r,e,n))||(i?xa(r,t):ya(r,t))}function Sa(r,e,t,n){return cr(r,"handleDoubleClickOn",e,t,n)||r.someProp("handleDoubleClick",i=>i(r,e,n))}function ka(r,e,t,n){return cr(r,"handleTripleClickOn",e,t,n)||r.someProp("handleTripleClick",i=>i(r,e,n))||Ca(r,t,n)}function Ca(r,e,t){if(t.button!=0)return!1;let n=Os(r,e,!0),i=r.state.doc;return n?(Pt(r,n,"pointer"),n instanceof C&&i.eq(r.state.doc)&&(r.input.mouseDown=new er(r,n)),!0):!1}function Os(r,e,t){let n=r.state.doc;if(e==-1)return n.inlineContent?C.create(n,0,n.content.size):null;let i=n.resolve(e);for(let s=i.depth+1;s>0;s--){let o=s>i.depth?i.nodeAfter:i.node(s),l=i.before(s);if(o.inlineContent)return C.create(n,l+1,l+1+o.content.size);if(t&&S.isSelectable(o))return S.create(n,l)}return null}function hr(r){return Nt(r)}var ws=G?"metaKey":"ctrlKey";$.mousedown=(r,e)=>{let t=e;r.input.shiftKey=t.shiftKey;let n=hr(r),i=Date.now(),s="singleClick";i-r.input.lastClick.time<500&&ga(t,r.input.lastClick)&&!t[ws]&&r.input.lastClick.button==t.button&&(r.input.lastClick.type=="singleClick"?s="doubleClick":r.input.lastClick.type=="doubleClick"&&(s="tripleClick")),r.input.lastClick={time:i,x:t.clientX,y:t.clientY,type:s,button:t.button},r.input.mouseDown&&r.input.mouseDown.done();let o=r.posAtCoords(Rt(t));o&&(s=="singleClick"?r.input.mouseDown=new Zn(r,o,t,!!n):(s=="doubleClick"?Sa:ka)(r,o.pos,o.inside,t)?t.preventDefault():me(r,"pointer"))};var _t=class{constructor(e){this.view=e,this.mightDrag=null,e.root.addEventListener("mouseup",this.up=this.up.bind(this)),e.root.addEventListener("mousemove",this.move=this.move.bind(this))}up(e){this.done()}move(e){e.buttons==0&&this.done()}done(){this.view.root.removeEventListener("mouseup",this.up),this.view.root.removeEventListener("mousemove",this.move),this.view.input.mouseDown==this&&(this.view.input.mouseDown=null)}delaySelUpdate(){return!1}},Zn=class extends _t{constructor(e,t,n,i){super(e),this.pos=t,this.event=n,this.flushed=i,this.delayedSelectionSync=!1,this.startDoc=e.state.doc,this.selectNode=!!n[ws],this.allowDefault=n.shiftKey;let s,o;if(t.inside>-1)s=e.state.doc.nodeAt(t.inside),o=t.inside;else{let c=e.state.doc.resolve(t.pos);s=c.parent,o=c.depth?c.before():0}let l=i?null:n.target,a=l?e.docView.nearestDesc(l,!0):null;this.target=a&&a.nodeDOM.nodeType==1?a.nodeDOM:null;let{selection:f}=e.state;n.button==0&&(s.type.spec.draggable&&s.type.spec.selectable!==!1||f instanceof S&&f.from<=o&&f.to>o)&&(this.mightDrag={node:s,pos:o,addAttr:!!(this.target&&!this.target.draggable),setUneditable:!!(this.target&&X&&!this.target.hasAttribute("contentEditable"))}),this.target&&this.mightDrag&&(this.mightDrag.addAttr||this.mightDrag.setUneditable)&&(this.view.domObserver.stop(),this.mightDrag.addAttr&&(this.target.draggable=!0),this.mightDrag.setUneditable&&setTimeout(()=>{this.view.input.mouseDown==this&&this.target.setAttribute("contentEditable","false")},20),this.view.domObserver.start()),me(e,"pointer")}done(){super.done(),this.mightDrag&&this.target&&(this.view.domObserver.stop(),this.mightDrag.addAttr&&this.target.removeAttribute("draggable"),this.mightDrag.setUneditable&&this.target.removeAttribute("contentEditable"),this.view.domObserver.start()),this.delayedSelectionSync&&setTimeout(()=>{this.view.isDestroyed||ge(this.view)})}up(e){if(this.done(),!this.view.dom.contains(e.target))return;let t=this.pos;this.view.state.doc!=this.startDoc&&(t=this.view.posAtCoords(Rt(e))),this.updateAllowDefault(e),this.allowDefault||!t?me(this.view,"pointer"):ba(this.view,t.pos,t.inside,e,this.selectNode)?e.preventDefault():e.button==0&&(this.flushed||L&&this.mightDrag&&!this.mightDrag.node.isAtom||P&&!this.view.state.selection.visible&&Math.min(Math.abs(t.pos-this.view.state.selection.from),Math.abs(t.pos-this.view.state.selection.to))<=2)?(Pt(this.view,k.near(this.view.state.doc.resolve(t.pos)),"pointer"),e.preventDefault()):me(this.view,"pointer")}move(e){this.updateAllowDefault(e),me(this.view,"pointer"),super.move(e)}updateAllowDefault(e){!this.allowDefault&&(Math.abs(this.event.x-e.clientX)>4||Math.abs(this.event.y-e.clientY)>4)&&(this.allowDefault=!0)}delaySelUpdate(){return this.allowDefault?(this.delayedSelectionSync=!0,!0):!1}},er=class extends _t{constructor(e,t){super(e),this.startSelection=t,this.startDoc=e.state.doc}move(e){if(e.buttons==0||this.view.isDestroyed||!this.view.state.doc.eq(this.startDoc)){this.done();return}e.preventDefault(),me(this.view,"pointer");let t=this.view.posAtCoords(Rt(e)),n=t&&Os(this.view,t.inside,!1);if(!n)return;let{doc:i}=this.view.state,s=this.startSelection,[o,l]=n.from{r.input.lastTouch=Date.now(),hr(r),me(r,"pointer")};$.touchmove=r=>{r.input.lastTouch=Date.now(),me(r,"pointer")};$.contextmenu=r=>hr(r);function Ns(r,e){return r.composing?!0:L&&Math.abs(Date.now()-r.input.compositionEndedAt)<500?(r.input.compositionEndedAt=-2e8,!0):!1}var Ma=pe?5e3:-1;q.compositionstart=q.compositionupdate=r=>{if(!r.composing){r.domObserver.flush();let{state:e}=r,t=e.selection.$to;if(e.selection instanceof C&&(e.storedMarks||!t.textOffset&&t.parentOffset&&t.nodeBefore.marks.some(n=>n.type.spec.inclusive===!1)||P&&is&&Oa(r)))r.markCursor=r.state.storedMarks||t.marks(),Nt(r,!0),r.markCursor=null;else if(Nt(r,!e.selection.empty),X&&e.selection.empty&&t.parentOffset&&!t.textOffset&&t.nodeBefore.marks.length){let n=r.domSelectionRange();for(let i=n.focusNode,s=n.focusOffset;i&&i.nodeType==1&&s!=0;){let o=s<0?i.lastChild:i.childNodes[s-1];if(!o)break;if(o.nodeType==3){let l=r.domSelection();l&&l.collapse(o,o.nodeValue.length);break}else i=o,s=-1}}r.input.composing=!0}Ds(r,Ma)};function Oa(r){let{focusNode:e,focusOffset:t}=r.domSelectionRange();if(!e||e.nodeType!=1||t>=e.childNodes.length)return!1;let n=e.childNodes[t];return n.nodeType==1&&n.contentEditable=="false"}q.compositionend=(r,e)=>{r.composing&&(r.input.composing=!1,r.input.compositionEndedAt=Date.now(),r.input.compositionPendingChanges=r.domObserver.pendingRecords().length?r.input.compositionID:0,r.input.compositionNode=null,r.input.badSafariComposition?r.domObserver.forceFlush():r.input.compositionPendingChanges&&Promise.resolve().then(()=>r.domObserver.flush()),r.input.compositionID++,Ds(r,20))};function Ds(r,e){clearTimeout(r.input.composingTimeout),e>-1&&(r.input.composingTimeout=setTimeout(()=>Nt(r),e))}function Ts(r){for(r.composing&&(r.input.composing=!1,r.input.compositionEndedAt=Date.now());r.input.compositionNodes.length>0;)r.input.compositionNodes.pop().markParentsDirty()}function wa(r){let e=r.domSelectionRange();if(!e.focusNode)return null;let t=Cl(e.focusNode,e.focusOffset),n=Ml(e.focusNode,e.focusOffset);if(t&&n&&t!=n){let i=n.pmViewDesc,s=r.domObserver.lastChangedTextNode;if(t==s||n==s)return s;if(!i||!i.isText(n.nodeValue))return n;if(r.input.compositionNode==n){let o=t.pmViewDesc;if(!(!o||!o.isText(t.nodeValue)))return n}}return t||n}function Nt(r,e=!1){if(!(pe&&r.domObserver.flushingSoon>=0)){if(r.domObserver.forceFlush(),Ts(r),e||r.docView&&r.docView.dirty){let t=sr(r),n=r.state.selection;return t&&!t.eq(n)?r.dispatch(r.state.tr.setSelection(t)):(r.markCursor||e)&&!n.$from.node(n.$from.sharedDepth(n.to)).inlineContent?r.dispatch(r.state.tr.deleteSelection()):r.updateState(r.state),!0}return!1}}function Na(r,e){if(!r.dom.parentNode)return;let t=r.dom.parentNode.appendChild(document.createElement("div"));t.appendChild(e),t.style.cssText="position: fixed; left: -10000px; top: 10px";let n=getSelection(),i=document.createRange();i.selectNodeContents(e),r.dom.blur(),n.removeAllRanges(),n.addRange(i),setTimeout(()=>{t.parentNode&&t.parentNode.removeChild(t),r.focus()},50)}var Dt=U&&De<15||lt&&Dl<604;$.copy=q.cut=(r,e)=>{let t=e,n=r.state.selection,i=t.type=="cut";if(n.empty)return;let s=Dt?null:t.clipboardData,o=n.content(),{dom:l,text:a}=lr(r,o);s?(t.preventDefault(),s.clearData(),s.setData("text/html",l.innerHTML),s.setData("text/plain",a)):Na(r,l),i&&r.dispatch(r.state.tr.deleteSelection().scrollIntoView().setMeta("uiEvent","cut"))};function Da(r){return r.openStart==0&&r.openEnd==0&&r.content.childCount==1?r.content.firstChild:null}function Ta(r,e){if(!r.dom.parentNode)return;let t=r.input.shiftKey||r.state.selection.$from.parent.type.spec.code,n=r.dom.parentNode.appendChild(document.createElement(t?"textarea":"div"));t||(n.contentEditable="true"),n.style.cssText="position: fixed; left: -10000px; top: 10px",n.focus();let i=r.input.shiftKey&&r.input.lastKeyCode!=45;setTimeout(()=>{r.focus(),n.parentNode&&n.parentNode.removeChild(n),t?Tt(r,n.value,null,i,e):Tt(r,n.textContent,n.innerHTML,i,e)},50)}function Tt(r,e,t,n,i){let s=ar(r,e,t,n,r.state.selection.$from);if(r.someProp("handlePaste",a=>a(r,i,s||x.empty)))return!0;if(!s)return!1;let o=Da(s),l=o?r.state.tr.replaceSelectionWith(o,n):r.state.tr.replaceSelection(s);return r.dispatch(l.scrollIntoView().setMeta("paste",!0).setMeta("uiEvent","paste")),!0}function Es(r){let e=r.getData("text/plain")||r.getData("Text");if(e)return e;let t=r.getData("text/uri-list");return t?t.replace(/\r?\n/g," "):""}q.paste=(r,e)=>{let t=e;if(r.composing&&!pe)return;let n=Dt?null:t.clipboardData,i=r.input.shiftKey&&r.input.lastKeyCode!=45;n&&Tt(r,Es(n),n.getData("text/html"),i,t)?t.preventDefault():Ta(r,t)};var Qt=class{constructor(e,t,n){this.slice=e,this.move=t,this.node=n}},Ea=G?"altKey":"ctrlKey";function As(r,e){let t;return r.someProp("dragCopies",n=>{t=t||n(e)}),t!=null?!t:!e[Ea]}$.dragstart=(r,e)=>{let t=e,n=r.input.mouseDown;if(n&&n.done(),!t.dataTransfer)return;let i=r.state.selection,s=i.empty?null:r.posAtCoords(Rt(t)),o;if(!(s&&s.pos>=i.from&&s.pos<=(i instanceof S?i.to-1:i.to))){if(n&&n.mightDrag)o=S.create(r.state.doc,n.mightDrag.pos);else if(t.target&&t.target.nodeType==1){let h=r.docView.nearestDesc(t.target,!0);h&&h.node.type.spec.draggable&&h!=r.docView&&(o=S.create(r.state.doc,h.posBefore))}}let l=(o||r.state.selection).content(),{dom:a,text:f,slice:c}=lr(r,l);(!t.dataTransfer.files.length||!P||rs>120)&&t.dataTransfer.clearData(),t.dataTransfer.setData(Dt?"Text":"text/html",a.innerHTML),t.dataTransfer.effectAllowed="copyMove",Dt||t.dataTransfer.setData("text/plain",f),r.dragging=new Qt(c,As(r,t),o)};$.dragend=r=>{let e=r.dragging;window.setTimeout(()=>{r.dragging==e&&(r.dragging=null)},50)};q.dragover=q.dragenter=(r,e)=>e.preventDefault();q.drop=(r,e)=>{try{Aa(r,e,r.dragging)}finally{r.dragging=null}};function Aa(r,e,t){if(!e.dataTransfer)return;let n=r.posAtCoords(Rt(e));if(!n)return;let i=r.state.doc.resolve(n.pos),s=t&&t.slice;s?r.someProp("transformPasted",d=>{s=d(s,r,!1)}):s=ar(r,Es(e.dataTransfer),Dt?null:e.dataTransfer.getData("text/html"),!1,i);let o=!!(t&&As(r,e));if(r.someProp("handleDrop",d=>d(r,e,s||x.empty,o))){e.preventDefault();return}if(!s)return;e.preventDefault();let l=s?kt(r.state.doc,i.pos,s):i.pos;l==null&&(l=i.pos);let a=r.state.tr;if(o){let{node:d}=t;d?d.replace(a):a.deleteSelection()}let f=a.mapping.map(l),c=s.openStart==0&&s.openEnd==0&&s.content.childCount==1,h=a.doc;if(c?a.replaceRangeWith(f,f,s.content.firstChild):a.replaceRange(f,f,s),a.doc.eq(h))return;let u=a.doc.resolve(f);if(c&&S.isSelectable(s.content.firstChild)&&u.nodeAfter&&u.nodeAfter.sameMarkup(s.content.firstChild))a.setSelection(new S(u));else{let d=a.mapping.map(l);a.mapping.maps[a.mapping.maps.length-1].forEach((p,m,g,b)=>d=b),a.setSelection(or(r,u,a.doc.resolve(d)))}r.focus(),r.dispatch(a.setMeta("uiEvent","drop"))}$.focus=r=>{r.input.lastFocus=Date.now(),r.focused||(r.domObserver.stop(),r.dom.classList.add("ProseMirror-focused"),r.domObserver.start(),r.focused=!0,setTimeout(()=>{r.docView&&r.hasFocus()&&!r.domObserver.currentSelection.eq(r.domSelectionRange())&&ge(r)},20))};$.blur=(r,e)=>{let t=e;r.focused&&(r.domObserver.stop(),r.dom.classList.remove("ProseMirror-focused"),r.domObserver.start(),t.relatedTarget&&r.dom.contains(t.relatedTarget)&&r.domObserver.currentSelection.clear(),r.focused=!1)};$.beforeinput=(r,e)=>{if(pe&&e.inputType=="deleteContentBackward"){r.domObserver.flushSoon();let{domChangeCount:n}=r.input;setTimeout(()=>{if(r.input.domChangeCount!=n||(r.dom.blur(),r.focus(),r.someProp("handleKeyDown",s=>s(r,Ve(8,"Backspace")))))return;let{$cursor:i}=r.state.selection;i&&i.pos>0&&r.dispatch(r.state.tr.delete(i.pos-1,i.pos).scrollIntoView())},50)}};for(let r in q)$[r]=q[r];function Et(r,e){if(r==e)return!0;for(let t in r)if(r[t]!==e[t])return!1;for(let t in e)if(!(t in r))return!1;return!0}var Zt=class r{constructor(e,t){this.toDOM=e,this.spec=t||qe,this.side=this.spec.side||0}map(e,t,n,i){let{pos:s,deleted:o}=e.mapResult(t.from+i,this.side<0?-1:1);return o?null:new te(s-n,s-n,this)}valid(){return!0}eq(e){return this==e||e instanceof r&&(this.spec.key&&this.spec.key==e.spec.key||this.toDOM==e.toDOM&&Et(this.spec,e.spec))}destroy(e){this.spec.destroy&&this.spec.destroy(e)}},$e=class r{constructor(e,t){this.attrs=e,this.spec=t||qe}map(e,t,n,i){let s=e.map(t.from+i,this.spec.inclusiveStart?-1:1)-n,o=e.map(t.to+i,this.spec.inclusiveEnd?1:-1)-n;return s>=o?null:new te(s,o,this)}valid(e,t){return t.from=e&&(!s||s(l.spec))&&n.push(l.copy(l.from+i,l.to+i))}for(let o=0;oe){let l=this.children[o]+1;this.children[o+2].findInner(e-l,t-l,n,i+l,s)}}map(e,t,n){return this==V||e.maps.length==0?this:this.mapInner(e,t,0,0,n||qe)}mapInner(e,t,n,i,s){let o;for(let l=0;l{let f=a+n,c;if(c=Rs(t,l,f)){for(i||(i=this.children.slice());sl&&h.to=e){this.children[l]==e&&(n=this.children[l+2]);break}let s=e+1,o=s+t.content.size;for(let l=0;ls&&a.type instanceof $e){let f=Math.max(s,a.from)-s,c=Math.min(o,a.to)-s;fi.map(e,t,qe));return r.from(n)}forChild(e,t){if(t.isLeaf)return J.empty;let n=[];for(let i=0;it instanceof J)?e:e.reduce((t,n)=>t.concat(n instanceof J?n:n.members),[]))}}forEachSet(e){for(let t=0;t{let g=m-p-(d-u);for(let b=0;bN+c-h)continue;let w=l[b]+c-h;d>=w?l[b+1]=u<=w?-2:-1:u>=c&&g&&(l[b]+=g,l[b+1]+=g)}h+=g}),c=t.maps[f].map(c,-1)}let a=!1;for(let f=0;f=n.content.size){a=!0;continue}let u=t.map(r[f+1]+s,-1),d=u-i,{index:p,offset:m}=n.content.findIndex(h),g=n.maybeChild(p);if(g&&m==h&&m+g.nodeSize==d){let b=l[f+2].mapInner(t,g,c+1,r[f]+s+1,o);b!=V?(l[f]=h,l[f+1]=d,l[f+2]=b):(l[f+1]=-2,a=!0)}else a=!0}if(a){let f=Ra(l,r,e,t,i,s,o),c=tn(f,n,0,o);e=c.local;for(let h=0;ht&&o.to{let f=Rs(r,l,a+t);if(f){s=!0;let c=tn(f,l,t+a+1,n);c!=V&&i.push(a,a+l.nodeSize,c)}});let o=Is(s?Ps(r):r,-t).sort(Ke);for(let l=0;l0;)e++;r.splice(e,0,t)}function vn(r){let e=[];return r.someProp("decorations",t=>{let n=t(r.state);n&&n!=V&&e.push(n)}),r.cursorWrapper&&e.push(J.create(r.state.doc,[r.cursorWrapper.deco])),en.from(e)}var Pa={childList:!0,characterData:!0,characterDataOldValue:!0,attributes:!0,attributeOldValue:!0,subtree:!0},Ba=U&&De<=11,nr=class{constructor(){this.anchorNode=null,this.anchorOffset=0,this.focusNode=null,this.focusOffset=0}set(e){this.anchorNode=e.anchorNode,this.anchorOffset=e.anchorOffset,this.focusNode=e.focusNode,this.focusOffset=e.focusOffset}clear(){this.anchorNode=this.focusNode=null}eq(e){return e.anchorNode==this.anchorNode&&e.anchorOffset==this.anchorOffset&&e.focusNode==this.focusNode&&e.focusOffset==this.focusOffset}},rr=class{constructor(e,t){this.view=e,this.handleDOMChange=t,this.queue=[],this.flushingSoon=-1,this.observer=null,this.currentSelection=new nr,this.onCharData=null,this.suppressingSelectionUpdates=!1,this.lastChangedTextNode=null,this.observer=window.MutationObserver&&new window.MutationObserver(n=>{for(let i=0;ii.type=="childList"&&i.removedNodes.length||i.type=="characterData"&&i.oldValue.length>i.target.nodeValue.length)?this.flushSoon():L&&e.composing&&n.some(i=>i.type=="childList"&&i.target.nodeName=="TR")?(e.input.badSafariComposition=!0,this.flushSoon()):this.flush()}),Ba&&(this.onCharData=n=>{this.queue.push({target:n.target,type:"characterData",oldValue:n.prevValue}),this.flushSoon()}),this.onSelectionChange=this.onSelectionChange.bind(this)}flushSoon(){this.flushingSoon<0&&(this.flushingSoon=window.setTimeout(()=>{this.flushingSoon=-1,this.flush()},20))}forceFlush(){this.flushingSoon>-1&&(window.clearTimeout(this.flushingSoon),this.flushingSoon=-1,this.flush())}start(){this.observer&&(this.observer.takeRecords(),this.observer.observe(this.view.dom,Pa)),this.onCharData&&this.view.dom.addEventListener("DOMCharacterDataModified",this.onCharData),this.connectSelection()}stop(){if(this.observer){let e=this.observer.takeRecords();if(e.length){for(let t=0;tthis.flush(),20)}this.observer.disconnect()}this.onCharData&&this.view.dom.removeEventListener("DOMCharacterDataModified",this.onCharData),this.disconnectSelection()}connectSelection(){this.view.dom.ownerDocument.addEventListener("selectionchange",this.onSelectionChange)}disconnectSelection(){this.view.dom.ownerDocument.removeEventListener("selectionchange",this.onSelectionChange)}suppressSelectionUpdates(){this.suppressingSelectionUpdates=!0,setTimeout(()=>this.suppressingSelectionUpdates=!1,50)}onSelectionChange(){if(Li(this.view)){if(this.suppressingSelectionUpdates)return ge(this.view);if(U&&De<=11&&!this.view.state.selection.empty){let e=this.view.domSelectionRange();if(e.focusNode&&He(e.focusNode,e.focusOffset,e.anchorNode,e.anchorOffset))return this.flushSoon()}this.flush()}}setCurSelection(){this.currentSelection.set(this.view.domSelectionRange())}ignoreSelectionChange(e){if(!e.focusNode)return!0;let t=new Set,n;for(let s=e.focusNode;s;s=ot(s))t.add(s);for(let s=e.anchorNode;s;s=ot(s))if(t.has(s)){n=s;break}let i=n&&this.view.docView.nearestDesc(n);if(i&&i.ignoreMutation({type:"selection",target:n.nodeType==3?n.parentNode:n}))return this.setCurSelection(),!0}pendingRecords(){if(this.observer)for(let e of this.observer.takeRecords())this.queue.push(e);return this.queue}flush(){let{view:e}=this;if(!e.docView||this.flushingSoon>-1)return;let t=this.pendingRecords();t.length&&(this.queue=[]);let n=e.domSelectionRange(),i=!this.suppressingSelectionUpdates&&!this.currentSelection.eq(n)&&Li(e)&&!this.ignoreSelectionChange(n),s=-1,o=-1,l=!1,a=[];if(e.editable)for(let c=0;cc.nodeName=="BR")&&(e.input.lastKeyCode==8||e.input.lastKeyCode==46||P&&(e.composing||e.input.compositionEndedAt>Date.now()-50)&&t.some(c=>c.type=="childList"&&c.removedNodes.length))){for(let c of a)if(c.nodeName=="BR"&&c.parentNode){let h=c.nextSibling;for(;h&&h.nodeType==1;){if(h.contentEditable=="false"){c.parentNode.removeChild(c);break}h=h.firstChild}}}else if(X&&a.length){let c=a.filter(h=>h.nodeName=="BR");if(c.length==2){let[h,u]=c;h.parentNode&&h.parentNode.parentNode==u.parentNode?u.remove():h.remove()}else{let{focusNode:h}=this.currentSelection;for(let u of c){let d=u.parentNode;d&&d.nodeName=="LI"&&(!h||Fa(e,h)!=d)&&u.remove()}}}let f=null;s<0&&i&&e.input.lastFocus>Date.now()-200&&Math.max(e.input.lastTouch,e.input.lastClick.time)-1||i)&&(s>-1&&(e.docView.markDirty(s,o),za(e)),e.input.badSafariComposition&&(e.input.badSafariComposition=!1,Va(e,a)),this.handleDOMChange(s,o,l,a),e.docView&&e.docView.dirty?e.updateState(e.state):this.currentSelection.eq(n)||ge(e),this.currentSelection.set(n))}registerMutation(e,t){if(t.indexOf(e.target)>-1)return null;let n=this.view.docView.nearestDesc(e.target);if(e.type=="attributes"&&(n==this.view.docView||e.attributeName=="contenteditable"||e.attributeName=="style"&&!e.oldValue&&!e.target.getAttribute("style"))||!n||n.ignoreMutation(e))return null;if(e.type=="childList"){for(let c=0;cs;b--){let N=i.childNodes[b-1],w=N.pmViewDesc;if(N.nodeName=="BR"&&!w){o=b;break}if(!w||w.size)break}let u=r.state.doc,d=r.someProp("domParser")||Xe.fromSchema(r.state.schema),p=u.resolve(l),m=null,g=d.parse(i,{topNode:p.parent,topMatch:p.parent.contentMatchAt(p.index()),topOpen:!0,from:s,to:o,preserveWhitespace:p.parent.type.whitespace=="pre"?"full":!0,findPositions:c,ruleFromNode:Wa(n),context:p});if(c&&c[0].pos!=null){let b=c[0].pos,N=c[1]&&c[1].pos;N==null&&(N=b),m={anchor:b+l,head:N+l}}return{doc:g,sel:m,from:l,to:a}}var Wa=r=>e=>{let t=e.pmViewDesc;if(t)return t.parseRule(r);if(e.nodeName=="BR"&&e.parentNode){if(L&&/^(ul|ol)$/i.test(e.parentNode.nodeName)){let n=document.createElement("div");return n.appendChild(document.createElement("li")),{skip:n}}else if(e.parentNode.lastChild==e||L&&/^(tr|table)$/i.test(e.parentNode.nodeName))return{ignore:!0}}else if(e.nodeName=="IMG"&&e.getAttribute("mark-placeholder"))return{ignore:!0};return null},Ja=/^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|img|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;function $a(r,e,t,n,i){let s=r.input.compositionPendingChanges||(r.composing?r.input.compositionID:0);if(r.input.compositionPendingChanges=0,e<0){let M=r.input.lastSelectionTime>Date.now()-50?r.input.lastSelectionOrigin:null,A=sr(r,M);if(A&&!r.state.selection.eq(A)){if(P&&pe&&r.input.lastKeyCode===13&&Date.now()-100Ao(r,Ve(13,"Enter"))))return;let K=r.state.tr.setSelection(A);M=="pointer"?K.setMeta("pointer",!0):M=="key"&&K.scrollIntoView(),s&&K.setMeta("composition",s),r.dispatch(K)}return}let o=r.state.doc.resolve(e),l=o.sharedDepth(t);e=o.before(l+1),t=r.state.doc.resolve(t).after(l+1);let a=r.state.selection,f=La(r,e,t,i),c=r.state.doc,h=c.slice(f.from,f.to),u,d;r.input.lastKeyCode===8&&Date.now()-100Date.now()-225||pe)&&i.some(M=>M.nodeType==1&&!Ja.test(M.nodeName))&&(!p||p.endA>=p.endB)&&r.someProp("handleKeyDown",M=>M(r,Ve(13,"Enter")))){r.input.lastIOSEnter=0;return}if(!p)if(n&&a instanceof C&&!a.empty&&a.$head.sameParent(a.$anchor)&&!r.composing&&!(f.sel&&f.sel.anchor!=f.sel.head))p={start:a.from,endA:a.to,endB:a.to};else{if(f.sel){let M=Xi(r,r.state.doc,f.sel);if(M&&!M.eq(r.state.selection)){let A=r.state.tr.setSelection(M);s&&A.setMeta("composition",s),r.dispatch(A)}}return}r.state.selection.fromr.state.selection.from&&p.start<=r.state.selection.from+2&&r.state.selection.from>=f.from?p.start=r.state.selection.from:p.endA=r.state.selection.to-2&&r.state.selection.to<=f.to&&(p.endB+=r.state.selection.to-p.endA,p.endA=r.state.selection.to)),U&&De<=11&&p.endB==p.start+1&&p.endA==p.start&&p.start>f.from&&f.doc.textBetween(p.start-f.from-1,p.start-f.from+1)==" \xA0"&&(p.start--,p.endA--,p.endB--);let m=f.doc.resolveNoCache(p.start-f.from),g=f.doc.resolveNoCache(p.endB-f.from),b=c.resolve(p.start),N=m.sameParent(g)&&m.parent.inlineContent&&b.end()>=p.endA;if((lt&&r.input.lastIOSEnter>Date.now()-225&&(!N||i.some(M=>M.nodeName=="DIV"||M.nodeName=="P"))||!N&&m.posM(r,Ve(13,"Enter")))){r.input.lastIOSEnter=0;return}if(r.state.selection.anchor>p.start&&Ka(c,p.start,p.endA,m,g)&&r.someProp("handleKeyDown",M=>M(r,Ve(8,"Backspace")))){pe&&P&&r.domObserver.suppressSelectionUpdates();return}P&&p.endB==p.start&&(r.input.lastChromeDelete=Date.now()),pe&&!N&&m.start()!=g.start()&&g.parentOffset==0&&m.depth==g.depth&&f.sel&&f.sel.anchor==f.sel.head&&f.sel.head==p.endA&&(p.endB-=2,g=f.doc.resolveNoCache(p.endB-f.from),setTimeout(()=>{r.someProp("handleKeyDown",function(M){return M(r,Ve(13,"Enter"))})},20));let w=p.start,re=p.endA,Ae=M=>{let A=M||r.state.tr.replace(w,re,f.doc.slice(p.start-f.from,p.endB-f.from));if(f.sel){let K=Xi(r,A.doc,f.sel);K&&!(P&&r.composing&&K.empty&&(p.start!=p.endB||r.input.lastChromeDeletege(r),20));let M=Ae(r.state.tr.delete(w,re)),A=c.resolve(p.start).marksAcross(c.resolve(p.endA));A&&M.ensureMarks(A),r.dispatch(M)}else if(p.endA==p.endB&&(Bt=qa(m.parent.content.cut(m.parentOffset,g.parentOffset),b.parent.content.cut(b.parentOffset,p.endA-b.start())))){let M=Ae(r.state.tr);Bt.type=="add"?M.addMark(w,re,Bt.mark):M.removeMark(w,re,Bt.mark),r.dispatch(M)}else if(m.parent.child(m.index()).isText&&m.index()==g.index()-(g.textOffset?0:1)){let M=m.parent.textBetween(m.parentOffset,g.parentOffset),A=()=>Ae(r.state.tr.insertText(M,w,re));r.someProp("handleTextInput",K=>K(r,w,re,M,A))||r.dispatch(A())}else r.dispatch(Ae());else r.dispatch(Ae())}function Xi(r,e,t){return Math.max(t.anchor,t.head)>e.content.size?null:or(r,e.resolve(t.anchor),e.resolve(t.head))}function qa(r,e){let t=r.firstChild.marks,n=e.firstChild.marks,i=t,s=n,o,l,a;for(let c=0;cc.mark(l.addToSet(c.marks));else if(i.length==0&&s.length==1)l=s[0],o="remove",a=c=>c.mark(l.removeFromSet(c.marks));else return null;let f=[];for(let c=0;ct||Fn(o,!0,!1)0&&(e||r.indexAfter(n)==r.node(n).childCount);)n--,i++,e=!1;if(t){let s=r.node(n).maybeChild(r.indexAfter(n));for(;s&&!s.isLeaf;)s=s.firstChild,i++}return i}function Ha(r,e,t,n,i){let s=r.findDiffStart(e,t),o=t+r.size,l=t+e.size;if(s==null)return null;let{a,b:f}=r.findDiffEnd(e,o,l);if(i=="end"){let c=Math.max(0,s-Math.min(a,f));n-=a+c-s}if(a=a?s-n:0;s-=c,f=s+(f-a),a=s}else if(f=f?s-n:0;s-=c,a=s+(a-f),f=s}return{start:s,endA:a,endB:f}}var Ua=ar,ja=Nt,nn=class{constructor(e,t){this._root=null,this.focused=!1,this.trackWrites=null,this.mounted=!1,this.markCursor=null,this.cursorWrapper=null,this.lastSelectedViewDesc=void 0,this.input=new _n,this.prevDirectPlugins=[],this.pluginViews=[],this.requiresGeckoHackNode=!1,this.dragging=null,this._props=t,this.state=t.state,this.directPlugins=t.plugins||[],this.directPlugins.forEach(ts),this.dispatch=this.dispatch.bind(this),this.dom=e&&e.mount||document.createElement("div"),e&&(e.appendChild?e.appendChild(this.dom):typeof e=="function"?e(this.dom):e.mount&&(this.mounted=!0)),this.editable=Zi(this),Qi(this),this.nodeViews=es(this),this.docView=Pi(this.state.doc,_i(this),vn(this),this.dom,this),this.domObserver=new rr(this,(n,i,s,o)=>$a(this,n,i,s,o)),this.domObserver.start(),ua(this),this.updatePluginViews()}get composing(){return this.input.composing}get props(){if(this._props.state!=this.state){let e=this._props;this._props={};for(let t in e)this._props[t]=e[t];this._props.state=this.state}return this._props}update(e){e.handleDOMEvents!=this._props.handleDOMEvents&&Qn(this);let t=this._props;this._props=e,e.plugins&&(e.plugins.forEach(ts),this.directPlugins=e.plugins),this.updateStateInner(e.state,t)}setProps(e){let t={};for(let n in this._props)t[n]=this._props[n];t.state=this.state;for(let n in e)t[n]=e[n];this.update(t)}updateState(e){this.updateStateInner(e,this._props)}updateStateInner(e,t){var n;let i=this.state,s=!1,o=!1;e.storedMarks&&this.composing&&(Ts(this),o=!0),this.state=e;let l=i.plugins!=e.plugins||this._props.plugins!=t.plugins;if(l||this._props.plugins!=t.plugins||this._props.nodeViews!=t.nodeViews){let d=es(this);Ya(d,this.nodeViews)&&(this.nodeViews=d,s=!0)}(l||t.handleDOMEvents!=this._props.handleDOMEvents)&&Qn(this),this.editable=Zi(this),Qi(this);let a=vn(this),f=_i(this),c=i.plugins!=e.plugins&&!i.doc.eq(e.doc)?"reset":e.scrollToSelection>i.scrollToSelection?"to selection":"preserve",h=s||!this.docView.matchesNode(e.doc,f,a);(h||!e.selection.eq(i.selection))&&(o=!0);let u=c=="preserve"&&o&&this.dom.style.overflowAnchor==null&&Al(this);if(o){this.domObserver.stop();let d=h&&(U||P)&&!this.composing&&!i.selection.empty&&!e.selection.empty&&Ga(i.selection,e.selection);if(h){let m=P?this.trackWrites=this.domSelectionRange().focusNode:null;this.composing&&(this.input.compositionNode=wa(this)),(s||!this.docView.update(e.doc,f,a,this))&&(this.docView.updateOuterDeco(f),this.docView.destroy(),this.docView=Pi(e.doc,f,a,this.dom,this)),m&&(!this.trackWrites||!this.dom.contains(this.trackWrites))&&(d=!0)}let p=this.input.mouseDown;d||!(p&&this.domObserver.currentSelection.eq(this.domSelectionRange())&&_l(this)&&p.delaySelUpdate())?ge(this,d):(gs(this,e.selection),this.domObserver.setCurSelection()),this.domObserver.start()}this.updatePluginViews(i),!((n=this.dragging)===null||n===void 0)&&n.node&&!i.doc.eq(e.doc)&&this.updateDraggedNode(this.dragging,i),c=="reset"?this.dom.scrollTop=0:c=="to selection"?this.scrollToSelection():u&&Il(u)}scrollToSelection(){let e=this.domSelectionRange().focusNode;if(!(!e||!this.dom.contains(e.nodeType==1?e:e.parentNode))){if(!this.someProp("handleScrollToSelection",t=>t(this)))if(this.state.selection instanceof S){let t=this.docView.domAfterPos(this.state.selection.from);t.nodeType==1&&Ti(this,t.getBoundingClientRect(),e)}else Ti(this,this.coordsAtPos(this.state.selection.head,1),e)}}destroyPluginViews(){let e;for(;e=this.pluginViews.pop();)e.destroy&&e.destroy()}updatePluginViews(e){if(!e||e.plugins!=this.state.plugins||this.directPlugins!=this.prevDirectPlugins){this.prevDirectPlugins=this.directPlugins,this.destroyPluginViews();for(let t=0;t0&&st.ownerDocument.getSelection()),this._root=t}return e||document}updateRoot(){this._root=null}posAtCoords(e){return Fl(this,e)}coordsAtPos(e,t=1){return cs(this,e,t)}domAtPos(e,t=0){return this.docView.domFromPos(e,t)}nodeDOM(e){let t=this.docView.descAt(e);return t?t.nodeDOM:null}posAtDOM(e,t,n=-1){let i=this.docView.posFromDOM(e,t,n);if(i==null)throw new RangeError("DOM position not inside the editor");return i}endOfTextblock(e,t){return $l(this,t||this.state,e)}pasteHTML(e,t){return Tt(this,"",e,!1,t||new ClipboardEvent("paste"))}pasteText(e,t){return Tt(this,e,null,!0,t||new ClipboardEvent("paste"))}serializeForClipboard(e){return lr(this,e)}destroy(){this.docView&&(da(this),this.destroyPluginViews(),this.mounted?(this.docView.update(this.state.doc,[],vn(this),this),this.dom.textContent=""):this.dom.parentNode&&this.dom.parentNode.removeChild(this.dom),this.docView.destroy(),this.docView=null,Sl())}get isDestroyed(){return this.docView==null}dispatchEvent(e){return ma(this,e)}domSelectionRange(){let e=this.domSelection();return e?L&&this.root.nodeType===11&&wl(this.dom.ownerDocument)==this.dom&&va(this,e)||e:{focusNode:null,focusOffset:0,anchorNode:null,anchorOffset:0}}domSelection(){return this.root.getSelection()}};nn.prototype.dispatch=function(r){let e=this._props.dispatchTransaction;e?e.call(this,r):this.updateState(this.state.apply(r))};function _i(r){let e=Object.create(null);return e.class="ProseMirror",e.contenteditable=String(r.editable),r.someProp("attributes",t=>{if(typeof t=="function"&&(t=t(r.state)),t)for(let n in t)n=="class"?e.class+=" "+t[n]:n=="style"?e.style=(e.style?e.style+";":"")+t[n]:!e[n]&&n!="contenteditable"&&n!="nodeName"&&(e[n]=String(t[n]))}),e.translate||(e.translate="no"),[te.node(0,r.state.doc.content.size,e)]}function Qi(r){if(r.markCursor){let e=document.createElement("img");e.className="ProseMirror-separator",e.setAttribute("mark-placeholder","true"),e.setAttribute("alt",""),r.cursorWrapper={dom:e,deco:te.widget(r.state.selection.from,e,{raw:!0,marks:r.markCursor})}}else r.cursorWrapper=null}function Zi(r){return!r.someProp("editable",e=>e(r.state)===!1)}function Ga(r,e){let t=Math.min(r.$anchor.sharedDepth(r.head),e.$anchor.sharedDepth(e.head));return r.$anchor.start(t)!=e.$anchor.start(t)}function es(r){let e=Object.create(null);function t(n){for(let i in n)Object.prototype.hasOwnProperty.call(e,i)||(e[i]=n[i])}return r.someProp("nodeViews",t),r.someProp("markViews",t),e}function Ya(r,e){let t=0,n=0;for(let i in r){if(r[i]!=e[i])return!0;t++}for(let i in e)n++;return t!=n}function ts(r){if(r.spec.state||r.spec.filterTransaction||r.spec.appendTransaction)throw new RangeError("Plugins passed directly to the view must not have a state component")}var vs={};j(vs,{collab:()=>_a,getVersion:()=>ef,rebaseSteps:()=>zs,receiveTransaction:()=>Qa,sendableSteps:()=>Za});var sn=class{constructor(e,t,n){this.step=e,this.inverted=t,this.origin=n}};function zs(r,e,t){for(let i=r.length-1;i>=0;i--)t.step(r[i].inverted);for(let i=0;inew at(e.version,[]),apply(t,n){let i=t.getMeta(ye);return i||(t.docChanged?new at(n.version,n.unconfirmed.concat(Xa(t))):n)}},config:e,historyPreserveItems:!0})}function Qa(r,e,t,n={}){let i=ye.getState(r),s=i.version+e.length,o=ye.get(r).spec.config.clientID,l=0;for(;lt.step),clientID:ye.get(r).spec.config.clientID,get origins(){return this._origins||(this._origins=e.unconfirmed.map(t=>t.origin))}}}function ef(r){return ye.getState(r).version}var qs={};j(qs,{closeHistory:()=>ff,history:()=>cf,isHistoryTransaction:()=>mf,redo:()=>$s,redoDepth:()=>pf,redoNoScroll:()=>uf,undo:()=>Js,undoDepth:()=>df,undoNoScroll:()=>hf});var on=200,B=function(){};B.prototype.append=function(e){return e.length?(e=B.from(e),!this.length&&e||e.length=t?B.empty:this.sliceInner(Math.max(0,e),Math.min(this.length,t))};B.prototype.get=function(e){if(!(e<0||e>=this.length))return this.getInner(e)};B.prototype.forEach=function(e,t,n){t===void 0&&(t=0),n===void 0&&(n=this.length),t<=n?this.forEachInner(e,t,n,0):this.forEachInvertedInner(e,t,n,0)};B.prototype.map=function(e,t,n){t===void 0&&(t=0),n===void 0&&(n=this.length);var i=[];return this.forEach(function(s,o){return i.push(e(s,o))},t,n),i};B.from=function(e){return e instanceof B?e:e&&e.length?new Fs(e):B.empty};var Fs=function(r){function e(n){r.call(this),this.values=n}r&&(e.__proto__=r),e.prototype=Object.create(r&&r.prototype),e.prototype.constructor=e;var t={length:{configurable:!0},depth:{configurable:!0}};return e.prototype.flatten=function(){return this.values},e.prototype.sliceInner=function(i,s){return i==0&&s==this.length?this:new e(this.values.slice(i,s))},e.prototype.getInner=function(i){return this.values[i]},e.prototype.forEachInner=function(i,s,o,l){for(var a=s;a=o;a--)if(i(this.values[a],l+a)===!1)return!1},e.prototype.leafAppend=function(i){if(this.length+i.length<=on)return new e(this.values.concat(i.flatten()))},e.prototype.leafPrepend=function(i){if(this.length+i.length<=on)return new e(i.flatten().concat(this.values))},t.length.get=function(){return this.values.length},t.depth.get=function(){return 0},Object.defineProperties(e.prototype,t),e}(B);B.empty=new Fs([]);var tf=function(r){function e(t,n){r.call(this),this.left=t,this.right=n,this.length=t.length+n.length,this.depth=Math.max(t.depth,n.depth)+1}return r&&(e.__proto__=r),e.prototype=Object.create(r&&r.prototype),e.prototype.constructor=e,e.prototype.flatten=function(){return this.left.flatten().concat(this.right.flatten())},e.prototype.getInner=function(n){return nl&&this.right.forEachInner(n,Math.max(i-l,0),Math.min(this.length,s)-l,o+l)===!1)return!1},e.prototype.forEachInvertedInner=function(n,i,s,o){var l=this.left.length;if(i>l&&this.right.forEachInvertedInner(n,i-l,Math.max(s,l)-l,o+l)===!1||s=s?this.right.slice(n-s,i-s):this.left.slice(n,s).append(this.right.slice(0,i-s))},e.prototype.leafAppend=function(n){var i=this.right.leafAppend(n);if(i)return new e(this.left,i)},e.prototype.leafPrepend=function(n){var i=this.left.leafPrepend(n);if(i)return new e(i,this.right)},e.prototype.appendInner=function(n){return this.left.depth>=Math.max(this.right.depth,n.depth)+1?new e(this.left,new e(this.right,n)):new e(this,n)},e}(B),dr=B;var nf=500,je=class r{constructor(e,t){this.items=e,this.eventCount=t}popEvent(e,t){if(this.eventCount==0)return null;let n=this.items.length;for(;;n--)if(this.items.get(n-1).selection){--n;break}let i,s;t&&(i=this.remapping(n,this.items.length),s=i.maps.length);let o=e.tr,l,a,f=[],c=[];return this.items.forEach((h,u)=>{if(!h.step){i||(i=this.remapping(n,u+1),s=i.maps.length),s--,c.push(h);return}if(i){c.push(new oe(h.map));let d=h.step.map(i.slice(s)),p;d&&o.maybeStep(d).doc&&(p=o.mapping.maps[o.mapping.maps.length-1],f.push(new oe(p,void 0,void 0,f.length+c.length))),s--,p&&i.appendMap(p,s)}else o.maybeStep(h.step);if(h.selection)return l=i?h.selection.map(i.slice(s)):h.selection,a=new r(this.items.slice(0,n).append(c.reverse().concat(f)),this.eventCount-1),!1},this.items.length,0),{remaining:a,transform:o,selection:l}}addTransform(e,t,n,i){let s=[],o=this.eventCount,l=this.items,a=!i&&l.length?l.get(l.length-1):null;for(let c=0;csf&&(l=rf(l,f),o-=f),new r(l.append(s),o)}remapping(e,t){let n=new Qe;return this.items.forEach((i,s)=>{let o=i.mirrorOffset!=null&&s-i.mirrorOffset>=e?n.maps.length-i.mirrorOffset:void 0;n.appendMap(i.map,o)},e,t),n}addMaps(e){return this.eventCount==0?this:new r(this.items.append(e.map(t=>new oe(t))),this.eventCount)}rebased(e,t){if(!this.eventCount)return this;let n=[],i=Math.max(0,this.items.length-t),s=e.mapping,o=e.steps.length,l=this.eventCount;this.items.forEach(u=>{u.selection&&l--},i);let a=t;this.items.forEach(u=>{let d=s.getMirror(--a);if(d==null)return;o=Math.min(o,d);let p=s.maps[d];if(u.step){let m=e.steps[d].invert(e.docs[d]),g=u.selection&&u.selection.map(s.slice(a+1,d));g&&l++,n.push(new oe(p,m,g))}else n.push(new oe(p))},i);let f=[];for(let u=t;unf&&(h=h.compress(this.items.length-n.length)),h}emptyItemCount(){let e=0;return this.items.forEach(t=>{t.step||e++}),e}compress(e=this.items.length){let t=this.remapping(0,e),n=t.maps.length,i=[],s=0;return this.items.forEach((o,l)=>{if(l>=e)i.push(o),o.selection&&s++;else if(o.step){let a=o.step.map(t.slice(n)),f=a&&a.getMap();if(n--,f&&t.appendMap(f,n),a){let c=o.selection&&o.selection.map(t.slice(n));c&&s++;let h=new oe(f.invert(),a,c),u,d=i.length-1;(u=i.length&&i[d].merge(h))?i[d]=u:i.push(h)}}else o.map&&n--},this.items.length,0),new r(dr.from(i.reverse()),s)}};je.empty=new je(dr.empty,0);function rf(r,e){let t;return r.forEach((n,i)=>{if(n.selection&&e--==0)return t=i,!1}),r.slice(t)}var oe=class r{constructor(e,t,n,i){this.map=e,this.step=t,this.selection=n,this.mirrorOffset=i}merge(e){if(this.step&&e.step&&!e.selection){let t=e.step.merge(this.step);if(t)return new r(t.getMap().invert(),t,this.selection)}}},le=class{constructor(e,t,n,i,s){this.done=e,this.undone=t,this.prevRanges=n,this.prevTime=i,this.prevComposition=s}},sf=20;function of(r,e,t,n){let i=t.getMeta(ae),s;if(i)return i.historyState;t.getMeta(Ws)&&(r=new le(r.done,r.undone,null,0,-1));let o=t.getMeta("appendedTransaction");if(t.steps.length==0)return r;if(o&&o.getMeta(ae))return o.getMeta(ae).redo?new le(r.done.addTransform(t,void 0,n,ln(e)),r.undone,Vs(t.mapping.maps),r.prevTime,r.prevComposition):new le(r.done,r.undone.addTransform(t,void 0,n,ln(e)),null,r.prevTime,r.prevComposition);if(t.getMeta("addToHistory")!==!1&&!(o&&o.getMeta("addToHistory")===!1)){let l=t.getMeta("composition"),a=r.prevTime==0||!o&&r.prevComposition!=l&&(r.prevTime<(t.time||0)-n.newGroupDelay||!lf(t,r.prevRanges)),f=o?pr(r.prevRanges,t.mapping):Vs(t.mapping.maps);return new le(r.done.addTransform(t,a?e.selection.getBookmark():void 0,n,ln(e)),je.empty,f,t.time,l??r.prevComposition)}else return(s=t.getMeta("rebased"))?new le(r.done.rebased(t,s),r.undone.rebased(t,s),pr(r.prevRanges,t.mapping),r.prevTime,r.prevComposition):new le(r.done.addMaps(t.mapping.maps),r.undone.addMaps(t.mapping.maps),pr(r.prevRanges,t.mapping),r.prevTime,r.prevComposition)}function lf(r,e){if(!e)return!1;if(!r.docChanged)return!0;let t=!1;return r.mapping.maps[0].forEach((n,i)=>{for(let s=0;s=e[s]&&(t=!0)}),t}function Vs(r){let e=[];for(let t=r.length-1;t>=0&&e.length==0;t--)r[t].forEach((n,i,s,o)=>e.push(s,o));return e}function pr(r,e){if(!r)return null;let t=[];for(let n=0;n{let i=ae.getState(t);if(!i||(r?i.undone:i.done).eventCount==0)return!1;if(n){let s=af(i,t,r);s&&n(e?s.scrollIntoView():s)}return!0}}var Js=an(!1,!0),$s=an(!0,!0),hf=an(!1,!1),uf=an(!0,!1);function df(r){let e=ae.getState(r);return e?e.done.eventCount:0}function pf(r){let e=ae.getState(r);return e?e.undone.eventCount:0}function mf(r){return r.getMeta(ae)!=null}var Hs={};j(Hs,{keydownHandler:()=>hn,keymap:()=>Cf});var xe={8:"Backspace",9:"Tab",10:"Enter",12:"NumLock",13:"Enter",16:"Shift",17:"Control",18:"Alt",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",44:"PrintScreen",45:"Insert",46:"Delete",59:";",61:"=",91:"Meta",92:"Meta",106:"*",107:"+",108:",",109:"-",110:".",111:"/",144:"NumLock",145:"ScrollLock",160:"Shift",161:"Shift",162:"Control",163:"Control",164:"Alt",165:"Alt",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},cn={48:")",49:"!",50:"@",51:"#",52:"$",53:"%",54:"^",55:"&",56:"*",57:"(",59:":",61:"+",173:"_",186:":",187:"+",188:"<",189:"_",190:">",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"'},gf=typeof navigator<"u"&&/Mac/.test(navigator.platform),yf=typeof navigator<"u"&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent);for(D=0;D<10;D++)xe[48+D]=xe[96+D]=String(D);var D;for(D=1;D<=24;D++)xe[D+111]="F"+D;var D;for(D=65;D<=90;D++)xe[D]=String.fromCharCode(D+32),cn[D]=String.fromCharCode(D);var D;for(fn in xe)cn.hasOwnProperty(fn)||(cn[fn]=xe[fn]);var fn;function Ks(r){var e=gf&&r.metaKey&&r.shiftKey&&!r.ctrlKey&&!r.altKey||yf&&r.shiftKey&&r.key&&r.key.length==1||r.key=="Unidentified",t=!e&&r.key||(r.shiftKey?cn:xe)[r.keyCode]||r.key||"Unidentified";return t=="Esc"&&(t="Escape"),t=="Del"&&(t="Delete"),t=="Left"&&(t="ArrowLeft"),t=="Up"&&(t="ArrowUp"),t=="Right"&&(t="ArrowRight"),t=="Down"&&(t="ArrowDown"),t}var xf=typeof navigator<"u"&&/Mac|iP(hone|[oa]d)/.test(navigator.platform),bf=typeof navigator<"u"&&/Win/.test(navigator.platform);function Sf(r){let e=r.split(/-(?!$)/),t=e[e.length-1];t=="Space"&&(t=" ");let n,i,s,o;for(let l=0;lFf,baseKeymap:()=>Lf,chainCommands:()=>un,createParagraphNear:()=>no,deleteSelection:()=>xr,exitCode:()=>to,joinBackward:()=>Gs,joinDown:()=>Nf,joinForward:()=>Qs,joinTextblockBackward:()=>Mf,joinTextblockForward:()=>Of,joinUp:()=>wf,lift:()=>Df,liftEmptyBlock:()=>ro,macBaseKeymap:()=>Mr,newlineInCode:()=>eo,pcBaseKeymap:()=>fe,selectAll:()=>so,selectNodeBackward:()=>Xs,selectNodeForward:()=>Zs,selectParentNode:()=>Ef,selectTextblockEnd:()=>fo,selectTextblockStart:()=>ao,setBlockType:()=>Rf,splitBlock:()=>Cr,splitBlockAs:()=>io,splitBlockKeepMarks:()=>Tf,toggleMark:()=>zf,wrapIn:()=>If});var xr=(r,e)=>r.selection.empty?!1:(e&&e(r.tr.deleteSelection().scrollIntoView()),!0);function js(r,e){let{$cursor:t}=r.selection;return!t||(e?!e.endOfTextblock("backward",r):t.parentOffset>0)?null:t}var Gs=(r,e,t)=>{let n=js(r,t);if(!n)return!1;let i=br(n);if(!i){let o=n.blockRange(),l=o&&Me(o);return l==null?!1:(e&&e(r.tr.lift(o,l).scrollIntoView()),!0)}let s=i.nodeBefore;if(oo(r,i,e,-1))return!0;if(n.parent.content.size==0&&(ft(s,"end")||S.isSelectable(s)))for(let o=n.depth;;o--){let l=tt(r.doc,n.before(o),n.after(o),x.empty);if(l&&l.slice.size1)break}return s.isAtom&&i.depth==n.depth-1?(e&&e(r.tr.delete(i.pos-s.nodeSize,i.pos).scrollIntoView()),!0):!1},Mf=(r,e,t)=>{let n=js(r,t);if(!n)return!1;let i=br(n);return i?Ys(r,i,e):!1},Of=(r,e,t)=>{let n=_s(r,t);if(!n)return!1;let i=Sr(n);return i?Ys(r,i,e):!1};function Ys(r,e,t){let n=e.nodeBefore,i=n,s=e.pos-1;for(;!i.isTextblock;s--){if(i.type.spec.isolating)return!1;let c=i.lastChild;if(!c)return!1;i=c}let o=e.nodeAfter,l=o,a=e.pos+1;for(;!l.isTextblock;a++){if(l.type.spec.isolating)return!1;let c=l.firstChild;if(!c)return!1;l=c}let f=tt(r.doc,s,a,x.empty);if(!f||f.from!=s||f instanceof F&&f.slice.size>=a-s)return!1;if(t){let c=r.tr.step(f);c.setSelection(C.create(c.doc,s)),t(c.scrollIntoView())}return!0}function ft(r,e,t=!1){for(let n=r;n;n=e=="start"?n.firstChild:n.lastChild){if(n.isTextblock)return!0;if(t&&n.childCount!=1)return!1}return!1}var Xs=(r,e,t)=>{let{$head:n,empty:i}=r.selection,s=n;if(!i)return!1;if(n.parent.isTextblock){if(t?!t.endOfTextblock("backward",r):n.parentOffset>0)return!1;s=br(n)}let o=s&&s.nodeBefore;return!o||!S.isSelectable(o)?!1:(e&&e(r.tr.setSelection(S.create(r.doc,s.pos-o.nodeSize)).scrollIntoView()),!0)};function br(r){if(!r.parent.type.spec.isolating)for(let e=r.depth-1;e>=0;e--){if(r.index(e)>0)return r.doc.resolve(r.before(e+1));if(r.node(e).type.spec.isolating)break}return null}function _s(r,e){let{$cursor:t}=r.selection;return!t||(e?!e.endOfTextblock("forward",r):t.parentOffset{let n=_s(r,t);if(!n)return!1;let i=Sr(n);if(!i)return!1;let s=i.nodeAfter;if(oo(r,i,e,1))return!0;if(n.parent.content.size==0&&(ft(s,"start")||S.isSelectable(s))){let o=tt(r.doc,n.before(),n.after(),x.empty);if(o&&o.slice.size{let{$head:n,empty:i}=r.selection,s=n;if(!i)return!1;if(n.parent.isTextblock){if(t?!t.endOfTextblock("forward",r):n.parentOffset=0;e--){let t=r.node(e);if(r.index(e)+1{let t=r.selection,n=t instanceof S,i;if(n){if(t.node.isTextblock||!Z(r.doc,t.from))return!1;i=t.from}else if(i=$t(r.doc,t.from,-1),i==null)return!1;if(e){let s=r.tr.join(i);n&&s.setSelection(S.create(s.doc,i-r.doc.resolve(i).nodeBefore.nodeSize)),e(s.scrollIntoView())}return!0},Nf=(r,e)=>{let t=r.selection,n;if(t instanceof S){if(t.node.isTextblock||!Z(r.doc,t.to))return!1;n=t.to}else if(n=$t(r.doc,t.to,1),n==null)return!1;return e&&e(r.tr.join(n).scrollIntoView()),!0},Df=(r,e)=>{let{$from:t,$to:n}=r.selection,i=t.blockRange(n),s=i&&Me(i);return s==null?!1:(e&&e(r.tr.lift(i,s).scrollIntoView()),!0)},eo=(r,e)=>{let{$head:t,$anchor:n}=r.selection;return!t.parent.type.spec.code||!t.sameParent(n)?!1:(e&&e(r.tr.insertText(` +`).scrollIntoView()),!0)};function kr(r){for(let e=0;e{let{$head:t,$anchor:n}=r.selection;if(!t.parent.type.spec.code||!t.sameParent(n))return!1;let i=t.node(-1),s=t.indexAfter(-1),o=kr(i.contentMatchAt(s));if(!o||!i.canReplaceWith(s,s,o))return!1;if(e){let l=t.after(),a=r.tr.replaceWith(l,l,o.createAndFill());a.setSelection(k.near(a.doc.resolve(l),1)),e(a.scrollIntoView())}return!0},no=(r,e)=>{let t=r.selection,{$from:n,$to:i}=t;if(t instanceof H||n.parent.inlineContent||i.parent.inlineContent)return!1;let s=kr(i.parent.contentMatchAt(i.indexAfter()));if(!s||!s.isTextblock)return!1;if(e){let o=(!n.parentOffset&&i.index(){let{$cursor:t}=r.selection;if(!t||t.parent.content.size)return!1;if(t.depth>1&&t.after()!=t.end(-1)){let s=t.before();if(he(r.doc,s))return e&&e(r.tr.split(s).scrollIntoView()),!0}let n=t.blockRange(),i=n&&Me(n);return i==null?!1:(e&&e(r.tr.lift(n,i).scrollIntoView()),!0)};function io(r){return(e,t)=>{if(e.selection instanceof S&&e.selection.node.isBlock){let{$from:d}=e.selection;return!d.parentOffset||!he(e.doc,d.pos)?!1:(t&&t(e.tr.split(d.pos).scrollIntoView()),!0)}if(!e.selection.$from.depth)return!1;let n=e.tr;!e.selection.empty&&(e.selection instanceof C||e.selection instanceof H)&&n.deleteSelection();let{$from:i}=n.selection,s=n.steps.length,o=[],l,a,f=!1,c=!1;for(let d=i.depth;;d--)if(i.node(d).isBlock){f=i.end(d)==i.pos+(i.depth-d),c=i.start(d)==i.pos-(i.depth-d),a=kr(i.node(d-1).contentMatchAt(i.indexAfter(d-1)));let m=r&&r(i.parent,f,i);o.unshift(m||(f&&a?{type:a}:null)),l=d;break}else{if(d==1)return!1;o.unshift(null)}let h=i.pos,u=he(n.doc,h,o.length,o);if(u||(o[0]=a?{type:a}:null,u=he(n.doc,h,o.length,o)),!u)return!1;if(n.split(h,o.length,o),!f&&c&&i.node(l).type!=a){let d=n.mapping.slice(s),p=d.map(i.before(l)),m=n.doc.resolve(p);a&&i.node(l-1).canReplaceWith(m.index(),m.index()+1,a)&&n.setNodeMarkup(d.map(i.before(l)),a)}return t&&t(n.scrollIntoView()),!0}}var Cr=io(),Tf=(r,e)=>Cr(r,e&&(t=>{let n=r.storedMarks||r.selection.$to.parentOffset&&r.selection.$from.marks();n&&t.ensureMarks(n),e(t)})),Ef=(r,e)=>{let{$from:t,to:n}=r.selection,i,s=t.sharedDepth(n);return s==0?!1:(i=t.before(s),e&&e(r.tr.setSelection(S.create(r.doc,i))),!0)},so=(r,e)=>(e&&e(r.tr.setSelection(new H(r.doc))),!0);function Af(r,e,t){let n=e.nodeBefore,i=e.nodeAfter,s=e.index();return!n||!i||!n.type.compatibleContent(i.type)?!1:!n.content.size&&e.parent.canReplace(s-1,s)?(t&&t(r.tr.delete(e.pos-n.nodeSize,e.pos).scrollIntoView()),!0):!e.parent.canReplace(s,s+1)||!(i.isTextblock||Z(r.doc,e.pos))?!1:(t&&t(r.tr.join(e.pos).scrollIntoView()),!0)}function oo(r,e,t,n){let i=e.nodeBefore,s=e.nodeAfter,o,l,a=i.type.spec.isolating||s.type.spec.isolating;if(!a&&Af(r,e,t))return!0;let f=!a&&e.parent.canReplace(e.index(),e.index()+1);if(f&&(o=(l=i.contentMatchAt(i.childCount)).findWrapping(s.type))&&l.matchType(o[0]||s.type).validEnd){if(t){let d=e.pos+s.nodeSize,p=y.empty;for(let b=o.length-1;b>=0;b--)p=y.from(o[b].create(null,p));p=y.from(i.copy(p));let m=r.tr.step(new E(e.pos-1,d,e.pos,d,new x(p,1,0),o.length,!0)),g=m.doc.resolve(d+2*o.length);g.nodeAfter&&g.nodeAfter.type==i.type&&Z(m.doc,g.pos)&&m.join(g.pos),t(m.scrollIntoView())}return!0}let c=s.type.spec.isolating||n>0&&a?null:k.findFrom(e,1),h=c&&c.$from.blockRange(c.$to),u=h&&Me(h);if(u!=null&&u>=e.depth)return t&&t(r.tr.lift(h,u).scrollIntoView()),!0;if(f&&ft(s,"start",!0)&&ft(i,"end")){let d=i,p=[];for(;p.push(d),!d.isTextblock;)d=d.lastChild;let m=s,g=1;for(;!m.isTextblock;m=m.firstChild)g++;if(d.canReplace(d.childCount,d.childCount,m.content)){if(t){let b=y.empty;for(let w=p.length-1;w>=0;w--)b=y.from(p[w].copy(b));let N=r.tr.step(new E(e.pos-p.length,e.pos+s.nodeSize,e.pos+g,e.pos+s.nodeSize-g,new x(b,p.length,0),0,!0));t(N.scrollIntoView())}return!0}}return!1}function lo(r){return function(e,t){let n=e.selection,i=r<0?n.$from:n.$to,s=i.depth;for(;i.node(s).isInline;){if(!s)return!1;s--}return i.node(s).isTextblock?(t&&t(e.tr.setSelection(C.create(e.doc,r<0?i.start(s):i.end(s)))),!0):!1}}var ao=lo(-1),fo=lo(1);function If(r,e=null){return function(t,n){let{$from:i,$to:s}=t.selection,o=i.blockRange(s),l=o&&ze(o,r,e);return l?(n&&n(t.tr.wrap(o,l).scrollIntoView()),!0):!1}}function Rf(r,e=null){return function(t,n){let i=!1;for(let s=0;s{if(i)return!1;if(!(!a.isTextblock||a.hasMarkup(r,e)))if(a.type==r)i=!0;else{let c=t.doc.resolve(f),h=c.index();i=c.parent.canReplaceWith(h,h+1,r)}})}if(!i)return!1;if(n){let s=t.tr;for(let o=0;o{if(l||!n&&a.isAtom&&a.isInline&&f>=s.pos&&f+a.nodeSize<=o.pos)return!1;l=a.inlineContent&&a.type.allowsMarkType(t)}),l)return!0}return!1}function Bf(r){let e=[];for(let t=0;t{if(s.isAtom&&s.content.size&&s.isInline&&o>=n.pos&&o+s.nodeSize<=i.pos)return o+1>n.pos&&e.push(new Fe(n,n.doc.resolve(o+1))),n=n.doc.resolve(o+1+s.content.size),!1}),n.poso.doc.rangeHasMark(d.$from.pos,d.$to.pos,r)):h=!c.every(d=>{let p=!1;return u.doc.nodesBetween(d.$from.pos,d.$to.pos,(m,g,b)=>{if(p)return!1;p=!r.isInSet(m.marks)&&!!b&&b.type.allowsMarkType(r)&&!(m.isText&&/^\s*$/.test(m.textBetween(Math.max(0,d.$from.pos-g),Math.min(m.nodeSize,d.$to.pos-g))))}),!p});for(let d=0;d{if(!t.isGeneric)return r(t);let n=[];for(let s=0;sn.push(f,c))}let i=[];for(let s=0;ss-o);for(let s=i.length-1;s>=0;s--)Z(t.doc,i[s])&&t.join(i[s]);r(t)}}function Ff(r,e){let t=Array.isArray(e)?n=>e.indexOf(n.type.name)>-1:e;return(n,i,s)=>r(n,i&&vf(i,t),s)}function un(...r){return function(e,t,n){for(let i=0;iuo,nodes:()=>ho,schema:()=>Gf});var Wf=["p",0],Jf=["blockquote",0],$f=["hr"],qf=["pre",["code",0]],Kf=["br"],ho={doc:{content:"block+"},paragraph:{content:"inline*",group:"block",parseDOM:[{tag:"p"}],toDOM(){return Wf}},blockquote:{content:"block+",group:"block",defining:!0,parseDOM:[{tag:"blockquote"}],toDOM(){return Jf}},horizontal_rule:{group:"block",parseDOM:[{tag:"hr"}],toDOM(){return $f}},heading:{attrs:{level:{default:1,validate:"number"}},content:"inline*",group:"block",defining:!0,parseDOM:[{tag:"h1",attrs:{level:1}},{tag:"h2",attrs:{level:2}},{tag:"h3",attrs:{level:3}},{tag:"h4",attrs:{level:4}},{tag:"h5",attrs:{level:5}},{tag:"h6",attrs:{level:6}}],toDOM(r){return["h"+r.attrs.level,0]}},code_block:{content:"text*",marks:"",group:"block",code:!0,defining:!0,parseDOM:[{tag:"pre",preserveWhitespace:"full"}],toDOM(){return qf}},text:{group:"inline"},image:{inline:!0,attrs:{src:{validate:"string"},alt:{default:null,validate:"string|null"},title:{default:null,validate:"string|null"}},group:"inline",draggable:!0,parseDOM:[{tag:"img[src]",getAttrs(r){return{src:r.getAttribute("src"),title:r.getAttribute("title"),alt:r.getAttribute("alt")}}}],toDOM(r){let{src:e,alt:t,title:n}=r.attrs;return["img",{src:e,alt:t,title:n}]}},hard_break:{inline:!0,group:"inline",selectable:!1,parseDOM:[{tag:"br"}],toDOM(){return Kf}}},Hf=["em",0],Uf=["strong",0],jf=["code",0],uo={link:{attrs:{href:{validate:"string"},title:{default:null,validate:"string|null"}},inclusive:!1,parseDOM:[{tag:"a[href]",getAttrs(r){return{href:r.getAttribute("href"),title:r.getAttribute("title")}}}],toDOM(r){let{href:e,title:t}=r.attrs;return["a",{href:e,title:t},0]}},em:{parseDOM:[{tag:"i"},{tag:"em"},{style:"font-style=italic"},{style:"font-style=normal",clearMark:r=>r.type.name=="em"}],toDOM(){return Hf}},strong:{parseDOM:[{tag:"strong"},{tag:"b",getAttrs:r=>r.style.fontWeight!="normal"&&null},{style:"font-weight=400",clearMark:r=>r.type.name=="strong"},{style:"font-weight",getAttrs:r=>/^(bold(er)?|[5-9]\d{2,})$/.test(r)&&null}],toDOM(){return Uf}},code:{code:!0,parseDOM:[{tag:"code"}],toDOM(){return jf}}},Gf=new pt({nodes:ho,marks:uo});var So={};j(So,{addListNodes:()=>Qf,bulletList:()=>go,liftListItem:()=>nc,listItem:()=>yo,orderedList:()=>mo,sinkListItem:()=>sc,splitListItem:()=>bo,splitListItemKeepMarks:()=>tc,wrapInList:()=>Zf,wrapRangeInList:()=>xo});var Yf=["ol",0],Xf=["ul",0],_f=["li",0],mo={attrs:{order:{default:1,validate:"number"}},parseDOM:[{tag:"ol",getAttrs(r){return{order:r.hasAttribute("start")?+r.getAttribute("start"):1}}}],toDOM(r){return r.attrs.order==1?Yf:["ol",{start:r.attrs.order},0]}},go={parseDOM:[{tag:"ul"}],toDOM(){return Xf}},yo={parseDOM:[{tag:"li"}],toDOM(){return _f},defining:!0};function Or(r,e){let t={};for(let n in r)t[n]=r[n];for(let n in e)t[n]=e[n];return t}function Qf(r,e,t){return r.append({ordered_list:Or(mo,{content:"list_item+",group:t}),bullet_list:Or(go,{content:"list_item+",group:t}),list_item:Or(yo,{content:e})})}function Zf(r,e=null){return function(t,n){let{$from:i,$to:s}=t.selection,o=i.blockRange(s);if(!o)return!1;let l=n?t.tr:null;return xo(l,o,r,e)?(n&&n(l.scrollIntoView()),!0):!1}}function xo(r,e,t,n=null){let i=!1,s=e,o=e.$from.doc;if(e.depth>=2&&e.$from.node(e.depth-1).type.compatibleContent(t)&&e.startIndex==0){if(e.$from.index(e.depth-1)==0)return!1;let a=o.resolve(e.start-2);s=new be(a,a,e.depth),e.endIndex=0;c--)s=y.from(t[c].type.create(t[c].attrs,s));r.step(new E(e.start-(n?2:0),e.end,e.start,e.end,new x(s,0,0),t.length,!0));let o=0;for(let c=0;c=i.depth-3;b--)h=y.from(i.node(b).copy(h));let d=i.indexAfter(-1){if(g>-1)return!1;b.isTextblock&&b.content.size==0&&(g=N+1)}),g>-1&&m.setSelection(k.near(m.doc.resolve(g))),n(m.scrollIntoView())}return!0}let a=s.pos==i.end()?l.contentMatchAt(0).defaultType:null,f=t.tr.delete(i.pos,s.pos),c=a?[e?{type:r,attrs:e}:null,{type:a}]:void 0;return he(f.doc,i.pos,2,c)?(n&&n(f.split(i.pos,2,c).scrollIntoView()),!0):!1}}function tc(r,e){let t=bo(r,e);return(n,i)=>t(n,i&&(s=>{let o=n.storedMarks||n.selection.$to.parentOffset&&n.selection.$from.marks();o&&s.ensureMarks(o),i(s)}))}function nc(r){return function(e,t){let{$from:n,$to:i}=e.selection,s=n.blockRange(i,o=>o.childCount>0&&o.firstChild.type==r);return s?t?n.node(s.depth-1).type==r?rc(e,t,r,s):ic(e,t,s):!0:!1}}function rc(r,e,t,n){let i=r.tr,s=n.end,o=n.$to.end(n.depth);sm;p--)d-=i.child(p).nodeSize,n.delete(d-1,d+1);let s=n.doc.resolve(t.start),o=s.nodeAfter;if(n.mapping.map(t.end)!=t.start+s.nodeAfter.nodeSize)return!1;let l=t.startIndex==0,a=t.endIndex==i.childCount,f=s.node(-1),c=s.index(-1);if(!f.canReplace(c+(l?0:1),c+1,o.content.append(a?y.empty:y.from(i))))return!1;let h=s.pos,u=h+o.nodeSize;return n.step(new E(h-(l?1:0),u+(a?1:0),h+1,u-1,new x((l?y.empty:y.from(i.copy(y.empty))).append(a?y.empty:y.from(i.copy(y.empty))),l?0:1,a?0:1),l?0:1)),e(n.scrollIntoView()),!0}function sc(r){return function(e,t){let{$from:n,$to:i}=e.selection,s=n.blockRange(i,f=>f.childCount>0&&f.firstChild.type==r);if(!s)return!1;let o=s.startIndex;if(o==0)return!1;let l=s.parent,a=l.child(o-1);if(a.type!=r)return!1;if(t){let f=a.lastChild&&a.lastChild.type==l.type,c=y.from(f?r.create():null),h=new x(y.from(r.create(null,y.from(l.type.create(null,c)))),f?3:1,0),u=s.start,d=s.end;t(e.tr.step(new E(u-(f?3:1),d,u,d,h,1,!0)).scrollIntoView())}return!0}}var No={};j(No,{InputRule:()=>ne,closeDoubleQuote:()=>Mo,closeSingleQuote:()=>wo,ellipsis:()=>hc,emDash:()=>cc,inputRules:()=>ac,openDoubleQuote:()=>Co,openSingleQuote:()=>Oo,smartQuotes:()=>uc,textblockTypeInputRule:()=>pc,undoInputRule:()=>fc,wrappingInputRule:()=>dc});var ne=class{constructor(e,t,n={}){this.match=e,this.match=e,this.handler=typeof t=="string"?oc(t):t,this.undoable=n.undoable!==!1,this.inCode=n.inCode||!1,this.inCodeMark=n.inCodeMark!==!1}};function oc(r){return function(e,t,n,i){let s=r;if(t[1]){let o=t[0].lastIndexOf(t[1]);s+=t[0].slice(o+t[1].length),n+=o;let l=n-i;l>0&&(s=t[0].slice(o-l,o)+s,n=i)}return e.tr.insertText(s,n,i)}}var lc=500;function ac({rules:r}){let e=new W({state:{init(){return null},apply(t,n){let i=t.getMeta(this);return i||(t.selectionSet||t.docChanged?null:n)}},props:{handleTextInput(t,n,i,s){return ko(t,n,i,s,r,e)},handleDOMEvents:{compositionend:t=>{setTimeout(()=>{let{$cursor:n}=t.state.selection;n&&ko(t,n.pos,n.pos,"",r,e)})}}},isInputRules:!0});return e}function ko(r,e,t,n,i,s){if(r.composing)return!1;let o=r.state,l=o.doc.resolve(e),a=l.parent.textBetween(Math.max(0,l.parentOffset-lc),l.parentOffset,null,"\uFFFC")+n;for(let f=0;fp.type.spec.code))continue;if(l.parent.type.spec.code){if(!c.inCode)continue}else if(c.inCode==="only")continue;let h=c.match.exec(a);if(!h||h[0].length{m.isInline&&m.marks.some(g=>g.type.spec.code)&&(p=!0)}),p)continue}let d=c.handler(o,h,u,t);if(d)return c.undoable&&d.setMeta(s,{transform:d,from:e,to:t,text:n}),r.dispatch(d),!0}return!1}var fc=(r,e)=>{let t=r.plugins;for(let n=0;n=0;a--)o.step(l.steps[a].invert(l.docs[a]));if(s.text){let a=o.doc.resolve(s.from).marks();o.replaceWith(s.from,s.to,r.schema.text(s.text,a))}else o.delete(s.from,s.to);e(o)}return!0}}return!1},cc=new ne(/--$/,"\u2014",{inCodeMark:!1}),hc=new ne(/\.\.\.$/,"\u2026",{inCodeMark:!1}),Co=new ne(/(?:^|[\s\{\[\(\<'"\u2018\u201C])(")$/,"\u201C",{inCodeMark:!1}),Mo=new ne(/"$/,"\u201D",{inCodeMark:!1}),Oo=new ne(/(?:^|[\s\{\[\(\<'"\u2018\u201C])(')$/,"\u2018",{inCodeMark:!1}),wo=new ne(/'$/,"\u2019",{inCodeMark:!1}),uc=[Co,Mo,Oo,wo];function dc(r,e,t=null,n){return new ne(r,(i,s,o,l)=>{let a=t instanceof Function?t(s):t,f=i.tr.delete(o,l),c=f.doc.resolve(o),h=c.blockRange(),u=h&&ze(h,e,a);if(!u)return null;f.wrap(h,u);let d=f.doc.resolve(o-1).nodeBefore;return d&&d.type==e&&Z(f.doc,o-1)&&(!n||n(s,d))&&f.join(o-1),f})}function pc(r,e,t=null){return new ne(r,(n,i,s,o)=>{let l=n.doc.resolve(s),a=t instanceof Function?t(i):t;return l.node(-1).canReplaceWith(l.index(-1),l.indexAfter(-1),e)?n.tr.delete(s,o).setBlockType(s,s,e,a):null})}var Do={};j(Do,{dropCursor:()=>mc});function mc(r={}){return new W({view(e){return new wr(e,r)}})}var wr=class{constructor(e,t){var n;this.editorView=e,this.cursorPos=null,this.element=null,this.timeout=-1,this.lastDragEvent=null,this.width=(n=t.width)!==null&&n!==void 0?n:1,this.color=t.color===!1?void 0:t.color||"black",this.class=t.class,this.handlers=["dragover","dragend","drop","dragleave"].map(i=>{let s=o=>{this[i](o)};return e.dom.addEventListener(i,s),{name:i,handler:s}})}destroy(){this.handlers.forEach(({name:e,handler:t})=>this.editorView.dom.removeEventListener(e,t))}update(e,t){if(this.cursorPos!=null&&t.doc!=e.state.doc)if(this.lastDragEvent){let n=this.computeTarget(this.lastDragEvent);n==this.cursorPos?this.updateOverlay():this.setCursor(n)}else this.updateOverlay()}setCursor(e){e!=this.cursorPos&&(this.cursorPos=e,e==null?(this.element.parentNode.removeChild(this.element),this.element=null):this.updateOverlay())}updateOverlay(){let e=this.editorView.state.doc.resolve(this.cursorPos),t=!e.parent.inlineContent,n,i=this.editorView.dom,s=i.getBoundingClientRect(),o=s.width/i.offsetWidth,l=s.height/i.offsetHeight;if(t){let h=e.nodeBefore,u=e.nodeAfter;if(h||u){let d=this.editorView.nodeDOM(this.cursorPos-(h?h.nodeSize:0));if(d){let p=d.getBoundingClientRect(),m=h?p.bottom:p.top;h&&u&&(m=(m+this.editorView.nodeDOM(this.cursorPos).getBoundingClientRect().top)/2);let g=this.width/2*l;n={left:p.left,right:p.right,top:m-g,bottom:m+g}}}}if(!n){let h=this.editorView.coordsAtPos(this.cursorPos),u=this.width/2*o;n={left:h.left-u,right:h.left+u,top:h.top,bottom:h.bottom}}let a=this.editorView.dom.offsetParent;this.element||(this.element=a.appendChild(document.createElement("div")),this.class&&(this.element.className=this.class),this.element.style.cssText="position: absolute; z-index: 50; pointer-events: none;",this.color&&(this.element.style.backgroundColor=this.color)),this.element.classList.toggle("prosemirror-dropcursor-block",t),this.element.classList.toggle("prosemirror-dropcursor-inline",!t);let f,c;if(!a||a==document.body&&getComputedStyle(a).position=="static")f=-pageXOffset,c=-pageYOffset;else{let h=a.getBoundingClientRect(),u=h.width/a.offsetWidth,d=h.height/a.offsetHeight;f=h.left-a.scrollLeft*u,c=h.top-a.scrollTop*d}this.element.style.left=(n.left-f)/o+"px",this.element.style.top=(n.top-c)/l+"px",this.element.style.width=(n.right-n.left)/o+"px",this.element.style.height=(n.bottom-n.top)/l+"px"}scheduleRemoval(e){clearTimeout(this.timeout),this.timeout=setTimeout(()=>this.setCursor(null),e)}computeTarget(e){let t=this.editorView.posAtCoords({left:e.clientX,top:e.clientY}),n=t&&t.inside>=0&&this.editorView.state.doc.nodeAt(t.inside),i=n&&n.type.spec.disableDropCursor,s=typeof i=="function"?i(this.editorView,t,e):i;if(!t||s)return null;let o=t.pos;if(this.editorView.dragging&&this.editorView.dragging.slice){let l=kt(this.editorView.state.doc,o,this.editorView.dragging.slice);l!=null&&(o=l)}return o}dragover(e){if(!this.editorView.editable)return;this.lastDragEvent=e;let t=this.computeTarget(e);t!=null&&(this.setCursor(t),this.scheduleRemoval(5e3))}dragend(){this.scheduleRemoval(20)}drop(){this.scheduleRemoval(20)}dragleave(e){this.editorView.dom.contains(e.relatedTarget)||this.setCursor(null)}};var Eo={};j(Eo,{GapCursor:()=>z,gapCursor:()=>xc});var z=class r extends k{constructor(e){super(e,e)}map(e,t){let n=e.resolve(t.map(this.head));return r.valid(n)?new r(n):k.near(n)}content(){return x.empty}eq(e){return e instanceof r&&e.head==this.head}toJSON(){return{type:"gapcursor",pos:this.head}}static fromJSON(e,t){if(typeof t.pos!="number")throw new RangeError("Invalid input for GapCursor.fromJSON");return new r(e.resolve(t.pos))}getBookmark(){return new Nr(this.anchor)}static valid(e){let t=e.parent;if(t.inlineContent||!gc(e)||!yc(e))return!1;let n=t.type.spec.allowGapCursor;if(n!=null)return n;let i=t.contentMatchAt(e.index()).defaultType;return i&&i.isTextblock}static findGapCursorFrom(e,t,n=!1){e:for(;;){if(!n&&r.valid(e))return e;let i=e.pos,s=null;for(let o=e.depth;;o--){let l=e.node(o);if(t>0?e.indexAfter(o)0){s=l.child(t>0?e.indexAfter(o):e.index(o)-1);break}else if(o==0)return null;i+=t;let a=e.doc.resolve(i);if(r.valid(a))return a}for(;;){let o=t>0?s.firstChild:s.lastChild;if(!o){if(s.isAtom&&!s.isText&&!S.isSelectable(s)){e=e.doc.resolve(i+s.nodeSize*t),n=!1;continue e}break}s=o,i+=t;let l=e.doc.resolve(i);if(r.valid(l))return l}return null}}};z.prototype.visible=!1;z.findFrom=z.findGapCursorFrom;k.jsonID("gapcursor",z);var Nr=class r{constructor(e){this.pos=e}map(e){return new r(e.map(this.pos))}resolve(e){let t=e.resolve(this.pos);return z.valid(t)?new z(t):k.near(t)}};function To(r){return r.isAtom||r.spec.isolating||r.spec.createGapCursor}function gc(r){for(let e=r.depth;e>=0;e--){let t=r.index(e),n=r.node(e);if(t==0){if(n.type.spec.isolating)return!0;continue}for(let i=n.child(t-1);;i=i.lastChild){if(i.childCount==0&&!i.inlineContent||To(i.type))return!0;if(i.inlineContent)return!1}}return!0}function yc(r){for(let e=r.depth;e>=0;e--){let t=r.indexAfter(e),n=r.node(e);if(t==n.childCount){if(n.type.spec.isolating)return!0;continue}for(let i=n.child(t);;i=i.firstChild){if(i.childCount==0&&!i.inlineContent||To(i.type))return!0;if(i.inlineContent)return!1}}return!0}function xc(){return new W({props:{decorations:Cc,createSelectionBetween(r,e,t){return e.pos==t.pos&&z.valid(t)?new z(t):null},handleClick:Sc,handleKeyDown:bc,handleDOMEvents:{beforeinput:kc}}})}var bc=hn({ArrowLeft:dn("horiz",-1),ArrowRight:dn("horiz",1),ArrowUp:dn("vert",-1),ArrowDown:dn("vert",1)});function dn(r,e){let t=r=="vert"?e>0?"down":"up":e>0?"right":"left";return function(n,i,s){let o=n.selection,l=e>0?o.$to:o.$from,a=o.empty;if(o instanceof C){if(!s.endOfTextblock(t)||l.depth==0)return!1;a=!1,l=n.doc.resolve(e>0?l.after():l.before())}let f=z.findGapCursorFrom(l,e,a);return f?(i&&i(n.tr.setSelection(new z(f))),!0):!1}}function Sc(r,e,t){if(!r||!r.editable)return!1;let n=r.state.doc.resolve(e);if(!z.valid(n))return!1;let i=r.posAtCoords({left:t.clientX,top:t.clientY});return i&&i.inside>-1&&S.isSelectable(r.state.doc.nodeAt(i.inside))?!1:(r.dispatch(r.state.tr.setSelection(new z(n))),!0)}function kc(r,e){if(e.inputType!="insertCompositionText"||!(r.state.selection instanceof z))return!1;let{$from:t}=r.state.selection,n=t.parent.contentMatchAt(t.index()).findWrapping(r.state.schema.nodes.text);if(!n)return!1;let i=y.empty;for(let o=n.length-1;o>=0;o--)i=y.from(n[o].createAndFill(null,i));let s=r.state.tr.replace(t.pos,t.pos,new x(i,0,0));return s.setSelection(C.near(s.doc.resolve(t.pos+1))),r.dispatch(s),!1}function Cc(r){if(!(r.selection instanceof z))return null;let e=document.createElement("div");return e.className="ProseMirror-gapcursor",J.create(r.doc,[te.widget(r.selection.head,e,{key:"gapcursor"})])}export{vs as collab,co as commands,Do as dropcursor,Eo as gapcursor,qs as history,No as inputrules,Hs as keymap,ei as model,po as schemaBasic,So as schemaList,wi as state,gi as transform,Bs as view}; diff --git a/tiles/tictactoe/icon.svg b/tiles/tictactoe/icon.svg new file mode 100644 index 0000000..bcd4768 --- /dev/null +++ b/tiles/tictactoe/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/tiles/tictactoe/index.html b/tiles/tictactoe/index.html new file mode 100644 index 0000000..95e46be --- /dev/null +++ b/tiles/tictactoe/index.html @@ -0,0 +1,214 @@ + + + + + + TicTacToe + + + +
TicTacToe
+
+
+ +
+ + + diff --git a/tiles/tictactoe/manifest.json b/tiles/tictactoe/manifest.json new file mode 100644 index 0000000..855dc66 --- /dev/null +++ b/tiles/tictactoe/manifest.json @@ -0,0 +1,20 @@ +{ + "name": "Untitled game", + "description": "TicTacToe for two, with an audience.", + "icons": [{ "src": "/icon.svg", "sizes": "any" }], + "sizing": { "width": 420, "height": 560 }, + "model": { + "id": "com.berjon.ziran.tictactoe", + "name": "TicTacToe", + "description": "TicTacToe for two, with an audience.", + "icons": [{ "src": "/icon.svg", "sizes": "any" }] + }, + "sync": { + "owner": "x", + "roles": { + "x": { "write": true, "admin": true }, + "o": { "write": true, "max": 1 }, + "watcher": { "write": false, "seesRoles": ["x", "o"] } + } + } +} diff --git a/tools/build-tiles.ts b/tools/build-tiles.ts index 0500188..7fcd0d1 100644 --- a/tools/build-tiles.ts +++ b/tools/build-tiles.ts @@ -33,6 +33,27 @@ async function buildTileDir(dir: string, out: string): Promise { } const tilesRoot = new URL('../tiles/', import.meta.url).pathname; +const toolsRoot = new URL('./', import.meta.url).pathname; + +// Vendor third-party libraries INTO the tiles that use them (tiles are +// self-contained; a tile's CRDT/editor is its own business and ships in the +// .tile file). Regenerated on every build. +const VENDORED: Array<{ entry: string; into: string[] }> = [ + { entry: 'entries/yjs-entry.ts', into: ['livetext/vendor/yjs.js'] }, + { entry: 'entries/pm-entry.ts', into: ['richtext/vendor/pm.js'] }, +]; +for (const v of VENDORED) { + for (const dest of v.into) { + const out = join(tilesRoot, dest); + await Deno.mkdir(join(out, '..'), { recursive: true }); + const cmd = await new Deno.Command(Deno.execPath(), { + args: ['bundle', '--platform', 'browser', '--minify', '-o', out, join(toolsRoot, v.entry)], + }).output(); + if (!cmd.success) throw new Error(`bundling ${v.entry} failed: ${new TextDecoder().decode(cmd.stderr)}`); + console.log(`vendored ${dest} (${(await Deno.stat(out)).size} bytes)`); + } +} + const outDir = join(tilesRoot, 'built'); await Deno.mkdir(outDir, { recursive: true }); for await (const entry of Deno.readDir(tilesRoot)) { diff --git a/tools/entries/pm-entry.ts b/tools/entries/pm-entry.ts new file mode 100644 index 0000000..0a812be --- /dev/null +++ b/tools/entries/pm-entry.ts @@ -0,0 +1,15 @@ +// ProseMirror bundle for the rich-text tile (vendored; tiles are +// self-contained). deno task build:tiles regenerates. +export * as state from 'prosemirror-state'; +export * as view from 'prosemirror-view'; +export * as model from 'prosemirror-model'; +export * as transform from 'prosemirror-transform'; +export * as collab from 'prosemirror-collab'; +export * as history from 'prosemirror-history'; +export * as keymap from 'prosemirror-keymap'; +export * as commands from 'prosemirror-commands'; +export * as schemaBasic from 'prosemirror-schema-basic'; +export * as schemaList from 'prosemirror-schema-list'; +export * as inputrules from 'prosemirror-inputrules'; +export * as dropcursor from 'prosemirror-dropcursor'; +export * as gapcursor from 'prosemirror-gapcursor'; diff --git a/tools/entries/yjs-entry.ts b/tools/entries/yjs-entry.ts new file mode 100644 index 0000000..857ac00 --- /dev/null +++ b/tools/entries/yjs-entry.ts @@ -0,0 +1,3 @@ +// Bundled into tiles that choose Yjs as their CRDT (vendored per-tile; tiles +// are self-contained). deno task build:tiles regenerates. +export * from 'yjs'; diff --git a/tools/mac-bundle.ts b/tools/mac-bundle.ts index b5be181..ee2b359 100644 --- a/tools/mac-bundle.ts +++ b/tools/mac-bundle.ts @@ -60,10 +60,18 @@ const exportedTypes = [ }, }, ]; +// ziran:// deep links (invitation accepts arrive as URL open-events). +const urlTypes = [{ + CFBundleURLName: 'com.berjon.ziran.link', + CFBundleURLSchemes: ['ziran'], +}]; + await run('plutil', ['-remove', 'CFBundleDocumentTypes', plist], true); await run('plutil', ['-remove', 'UTExportedTypeDeclarations', plist], true); +await run('plutil', ['-remove', 'CFBundleURLTypes', plist], true); await run('plutil', ['-insert', 'CFBundleDocumentTypes', '-json', JSON.stringify(documentTypes), plist]); await run('plutil', ['-insert', 'UTExportedTypeDeclarations', '-json', JSON.stringify(exportedTypes), plist]); +await run('plutil', ['-insert', 'CFBundleURLTypes', '-json', JSON.stringify(urlTypes), plist]); console.log('document types declared'); // 3. Tell LaunchServices. diff --git a/vendor/web-tiles/sync.js b/vendor/web-tiles/sync.js new file mode 100644 index 0000000..4adda0d --- /dev/null +++ b/vendor/web-tiles/sync.js @@ -0,0 +1,110 @@ +// Ziran tp-sync: an opaque, attributed, ordered collaboration channel for +// tiles. Like store.js, it rides the tp-data tiles protocol — the tile never +// learns how messages travel, who is invited, or how rights are enforced. +// Membership and invitations live entirely in the runtime chrome; there is +// deliberately NO membership-mutation surface here. +// +// import { sync } from '/.well-known/web-tiles/sync.js'; +// sync.onMessage(({ author, role, seq, bytes }) => …); // durable, ordered +// sync.send(bytes); // append to the log +// sync.onEphemeral(({ author, role, bytes }) => …); // presence lane +// sync.sendEphemeral(bytes); // not stored +// sync.onRoster((list) => …); sync.roster(); // role-filtered +// sync.onReady((me) => …); sync.me(); // {did, handle, role} +// +// `bytes` is always a Uint8Array; the tile owns its meaning (CRDT deltas, +// game moves, whatever). Attribution is runtime-attested: `author` is a DID +// the runtime verified, so a tile can safely reject messages that a given +// role/author is not allowed to make. + +import { addDataHandler, listen, sendData } from '/.well-known/web-tiles/data.js'; + +const messageHandlers = new Set(); +const ephemeralHandlers = new Set(); +const rosterHandlers = new Set(); +const readyHandlers = new Set(); + +let ready = false; +let me = null; +let currentRoster = []; +let seq = 0; // strict server order; runtime tags every durable message + +function toBytes(data) { + if (data == null) return new Uint8Array(0); + if (data instanceof Uint8Array) return data; + if (data instanceof ArrayBuffer) return new Uint8Array(data); + if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + throw new TypeError('sync payloads must be Uint8Array / ArrayBuffer'); +} + +addDataHandler((payload) => { + const msg = payload && payload.ziranSync; + if (!msg) return; + switch (msg.kind) { + case 'ready': + ready = true; + me = msg.me; + currentRoster = msg.roster ?? []; + for (const h of readyHandlers) h(me); + for (const h of rosterHandlers) h(currentRoster); + break; + case 'message': { + // Attach-replay and live delivery can overlap: drop duplicates so the + // tile sees each seq exactly once, in order. + if (typeof msg.seq === 'number') { + if (msg.seq <= seq) break; + seq = msg.seq; + } + const ev = { author: msg.author, role: msg.role, seq: msg.seq, bytes: toBytes(msg.data) }; + for (const h of messageHandlers) h(ev); + break; + } + case 'ephemeral': { + const ev = { author: msg.author, role: msg.role, bytes: toBytes(msg.data) }; + for (const h of ephemeralHandlers) h(ev); + break; + } + case 'roster': + currentRoster = msg.roster ?? []; + if (msg.me) me = msg.me; + for (const h of rosterHandlers) h(currentRoster); + break; + } +}); +listen(); +// Announce the tile is wired and ask the runtime for the opening state. +sendData({ ziranSync: { kind: 'hello' } }); + +function on(set, handler) { + set.add(handler); + return () => set.delete(handler); +} + +export const sync = { + /** Append a durable, ordered, attributed message to the document log. */ + send(data) { + sendData({ ziranSync: { kind: 'send', data: toBytes(data) } }); + }, + /** Send a presence-lane message: fanned out live to peers, never stored. */ + sendEphemeral(data) { + sendData({ ziranSync: { kind: 'ephemeral', data: toBytes(data) } }); + }, + onMessage: (h) => on(messageHandlers, h), + onEphemeral: (h) => on(ephemeralHandlers, h), + onRoster: (h) => on(rosterHandlers, h), + /** Fires once the channel is attached; also immediately if already ready. */ + onReady(h) { + if (ready) h(me); + return on(readyHandlers, h); + }, + /** Current participants visible to this role: [{ did, handle, role }]. */ + roster: () => currentRoster.slice(), + /** This participant: { did, handle, role } — null until ready. */ + me: () => me, + get ready() { + return ready; + }, + get lastSeq() { + return seq; + }, +}; diff --git a/website/Caddyfile b/website/Caddyfile new file mode 100644 index 0000000..d1831fb --- /dev/null +++ b/website/Caddyfile @@ -0,0 +1,15 @@ +# Internal vhost — served only behind the supramundane front proxy. +# Public domain, TLS, and privacy headers are handled by the front +# (see sites/ziran-web.caddy on the front, generated by `sm deploy`). +:80 { + # Canonical host: www → apex. + @www header_regexp host Host ^www\.(.+)$ + redir @www https://{re.host.1}{uri} permanent + + # Share links live on the relay; keep pretty ziran.space/i/… links working. + redir /i/* https://relay.ziran.space{uri} temporary + + root * /srv + try_files {path} /index.html + file_server +} diff --git a/website/Dockerfile b/website/Dockerfile new file mode 100644 index 0000000..2095e09 --- /dev/null +++ b/website/Dockerfile @@ -0,0 +1,3 @@ +FROM caddy:2.11.2-alpine +COPY site/ /srv/ +COPY Caddyfile /etc/caddy/Caddyfile diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..3eb9eab --- /dev/null +++ b/website/README.md @@ -0,0 +1,14 @@ +# ziran-web — sm service + +The ziran.space website: a static, self-contained landing page (no build step, +no trackers, no secrets). + +Deploy: + +```sh +cd website +sm deploy # or: sm deploy --local (serves ziran.bast) +``` + +`/i/` share links redirect to the relay (relay.ziran.space), so invite +links can be written against the bare domain. diff --git a/website/compose.yaml b/website/compose.yaml new file mode 100644 index 0000000..21d8d7e --- /dev/null +++ b/website/compose.yaml @@ -0,0 +1,12 @@ +services: + ziran-web: + build: . + restart: unless-stopped + container_name: ziran-web + networks: + - default + +networks: + default: + name: supramundane + external: true diff --git a/website/service.json b/website/service.json new file mode 100644 index 0000000..ca81879 --- /dev/null +++ b/website/service.json @@ -0,0 +1,11 @@ +{ + "name": "ziran-web", + "type": "static", + "domains": { + "local": ["ziran.bast", "www.ziran.bast"], + "prod": ["ziran.space", "www.ziran.space"] + }, + "headers": true, + "upstreamPort": 80, + "data": false +} diff --git a/website/site/index.html b/website/site/index.html new file mode 100644 index 0000000..6e77506 --- /dev/null +++ b/website/site/index.html @@ -0,0 +1,430 @@ + + + + + +Ziran — all your documents, one medium + + + + + + + + +
+ +
+
+
+

The document stack is fragmented.
It doesn't have to be.

+
+ PDF Word Google Docs Excel Sheets + PowerPoint Keynote tiles +
+

+ Text, tables, decks, games, anything: a tile is one file that does what all of + those do, because it brings its own editor along. Open it in ten years and it + still knows how to edit itself. +

+
+
+ +
+
+

Documents that carry their own editor

+
+
+

Self-editing

+
+

+ A tile packs content and editor together, addressed by cryptographic + fingerprint. If the fingerprint matches, nothing has tampered with your + document since it was written, and no app upgrade can orphan it. +

+

+ Any document can become a model: the editor without its data, a fresh + starting point you keep in your library. +

+
+
+
+

Local-first

+
+

+ Your documents are files on your machine. No cloud required, no account + required, nothing phones home. Ziran works entirely offline; the network + is something you opt into, one document at a time. +

+
+
+
+

Safe by construction

+
+

+ Tiles run in strict isolation: a document can edit itself, and that is all + it can do. It never sees your other documents, your identity, or the + network. The runtime holds the keys — tiles never do. +

+
+
+
+
+
+ +
+
+

Together, without a middleman that reads

+

+ Go live on any document and invite people by their AT handle, the identity they + already have. Accepting downloads the document to their machine first; then it + syncs, live. +

+
    +
  • + + End-to-end encrypted. Live sessions use MLS group + encryption. The relay orders and stores ciphertext. It can never read your + documents. +
  • +
  • + + Roles the document defines. An editor, a reviewer, one + chess opponent and any number of watchers: each document declares its roles + and enforces what they mean. +
  • +
  • + + Everyone keeps a real copy. Collaboration is between + copies you each own, not access to someone else's server. Leave any time; + your document stays yours. +
  • +
+
+
+ +
+
+

Get Ziran

+

+ Ziran is a desktop app: one small download, everything included. +

+
+ macOS · Apple Silicon & Intel + Windows · x64 + Linux · x64 & arm64 +
+

+ The first public build is being prepared. In the meantime, Ziran is being built + in the open; follow along at berjon.com. +

+
+
+
+ +
+
+ + + ziran + + Local-first · end-to-end encrypted · no account required + Made by Robin Berjon +
+
+ + +
+
+ +
+

All your documents. One medium.

+

+ Ziran is a local-first document environment built on tiles: documents that carry + their own editor, live on your machine, and sync end-to-end encrypted the moment + you invite someone in. +

+ +
+ +
+