diff --git a/package-lock.json b/package-lock.json index 27d8650..2da4406 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "fflate": "^0.8.2", "idb": "^8.0.3", "minimatch": "^10.2.2", + "node-datachannel": "^0.32.1", "octagonal-wheels": "^0.1.45", "pouchdb-adapter-leveldb": "^9.0.0", "qrcode-generator": "^1.4.4", @@ -7245,6 +7246,12 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -7595,6 +7602,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -7604,6 +7626,15 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -8092,7 +8123,6 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -8890,6 +8920,15 @@ "bare-events": "^2.7.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -9204,6 +9243,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -9468,6 +9513,12 @@ "node": ">= 14" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -9909,6 +9960,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/interface-datastore": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/interface-datastore/-/interface-datastore-8.3.2.tgz", @@ -11649,6 +11706,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -11711,6 +11780,12 @@ "dev": true, "license": "MIT" }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/modern-tar": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.5.tgz", @@ -11834,6 +11909,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/napi-macros": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", @@ -11855,6 +11936,31 @@ "node": ">= 0.4.0" } }, + "node_modules/node-abi": { + "version": "3.88.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.88.0.tgz", + "integrity": "sha512-At6b4UqIEVudaqPsXjmUO1r/N5BUr4yhDGs5PkBE8/oG5+TfLPhFechiskFsnT6Ql0VfUXbalUUCbfXxtj7K+w==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-datachannel": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/node-datachannel/-/node-datachannel-0.32.1.tgz", + "integrity": "sha512-r4UdtA0lCsz6XrG84pJ6lntAyw/MHpmBOhEkg5UQcmWTEpANqCPkMos6rj/QZDdq3GBUsdI/wst5acwWUiibCA==", + "hasInstallScript": true, + "license": "MPL 2.0", + "dependencies": { + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": ">=18.20.0" + } + }, "node_modules/node-fetch": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", @@ -12053,7 +12159,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -13028,6 +13133,119 @@ "integrity": "sha512-fXqsVn+rmlPtxaAIGaQP5TkiaT39OMwvMk+ScLLtHrmfXD2KBO6fe/qBl38N/rpTn0h/A058dPN4fLAHt550zA==", "dev": true }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/prebuild-install/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/prebuild-install/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/prebuild-install/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -13171,7 +13389,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -13251,6 +13468,30 @@ "integrity": "sha512-Mt2NznMgepLfORijhQMncE26IhkmjEphig+/1fKC0OtaKwys/gpvpmswSjoN01SS+VO951mj0L4VIDXdXsjnfA==", "license": "Apache-2.0 OR MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -13682,7 +13923,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -13899,6 +14139,51 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -15221,6 +15506,18 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -16621,7 +16918,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-stream": { @@ -21716,6 +22012,11 @@ "readdirp": "^4.0.1" } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -21960,11 +22261,24 @@ "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", "dev": true }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, "deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==" }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -22280,7 +22594,6 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -22840,6 +23153,11 @@ "bare-events": "^2.7.0" } }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, "expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -23072,6 +23390,11 @@ "signal-exit": "^4.0.1" } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -23245,6 +23568,11 @@ "debug": "^4.3.4" } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -23533,6 +23861,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "interface-datastore": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/interface-datastore/-/interface-datastore-8.3.2.tgz", @@ -24747,6 +25080,11 @@ "mime-db": "1.52.0" } }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, "minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -24787,6 +25125,11 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "dev": true }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "modern-tar": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.5.tgz", @@ -24870,6 +25213,11 @@ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true }, + "napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, "napi-macros": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", @@ -24886,6 +25234,22 @@ "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==" }, + "node-abi": { + "version": "3.88.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.88.0.tgz", + "integrity": "sha512-At6b4UqIEVudaqPsXjmUO1r/N5BUr4yhDGs5PkBE8/oG5+TfLPhFechiskFsnT6Ql0VfUXbalUUCbfXxtj7K+w==", + "requires": { + "semver": "^7.3.5" + } + }, + "node-datachannel": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/node-datachannel/-/node-datachannel-0.32.1.tgz", + "integrity": "sha512-r4UdtA0lCsz6XrG84pJ6lntAyw/MHpmBOhEkg5UQcmWTEpANqCPkMos6rj/QZDdq3GBUsdI/wst5acwWUiibCA==", + "requires": { + "prebuild-install": "^7.1.3" + } + }, "node-fetch": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", @@ -25012,7 +25376,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "requires": { "wrappy": "1" } @@ -25699,6 +26062,84 @@ "integrity": "sha512-fXqsVn+rmlPtxaAIGaQP5TkiaT39OMwvMk+ScLLtHrmfXD2KBO6fe/qBl38N/rpTn0h/A058dPN4fLAHt550zA==", "dev": true }, + "prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "dependencies": { + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + } + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -25805,7 +26246,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -25855,6 +26295,24 @@ "resolved": "https://registry.npmjs.org/race-signal/-/race-signal-1.1.3.tgz", "integrity": "sha512-Mt2NznMgepLfORijhQMncE26IhkmjEphig+/1fKC0OtaKwys/gpvpmswSjoN01SS+VO951mj0L4VIDXdXsjnfA==" }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + } + } + }, "readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -26139,8 +26597,7 @@ "semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==" }, "serialize-error": { "version": "12.0.0", @@ -26282,6 +26739,21 @@ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -27057,6 +27529,14 @@ } } }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -27856,8 +28336,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "write-stream": { "version": "0.4.3", diff --git a/package.json b/package.json index 458fbf8..5086786 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ "fflate": "^0.8.2", "idb": "^8.0.3", "minimatch": "^10.2.2", + "node-datachannel": "^0.32.1", "octagonal-wheels": "^0.1.45", "pouchdb-adapter-leveldb": "^9.0.0", "qrcode-generator": "^1.4.4", diff --git a/src/apps/cli/README.md b/src/apps/cli/README.md index 83bc7d9..9f53639 100644 --- a/src/apps/cli/README.md +++ b/src/apps/cli/README.md @@ -148,6 +148,9 @@ Options: Commands: init-settings [path] Create settings JSON from DEFAULT_SETTINGS sync Run one replication cycle and exit + p2p-peers Show discovered peers as [peer] + p2p-sync Synchronise with specified peer-id or peer-name + p2p-host Start P2P host mode and wait until interrupted (Ctrl+C) push Push local file into local database path pull Pull file from local database into local file pull-rev Pull specific revision into local file @@ -177,6 +180,32 @@ npm run --silent cli -- [database-path] [options] [command] [command-args] ``` Note: `*` indicates if the file has conflicts. +##### p2p-peers + +`p2p-peers ` waits for the specified number of seconds, then prints each discovered peer on a separate line: + +```text +[peer] +``` + +Use this command to select a target for `p2p-sync`. + +##### p2p-sync + +`p2p-sync ` discovers peers up to the specified timeout and synchronises with the selected peer. + +- `` accepts either `peer-id` or `peer-name` from `p2p-peers` output. +- On success, the command prints a completion message to standard error and exits with status code `0`. +- On failure, the command prints an error message and exits non-zero. + +##### p2p-host + +`p2p-host` starts the local P2P host and keeps running until interrupted. + +- Other peers can discover and synchronise with this host while it is running. +- Stop the host with `Ctrl+C`. +- In CLI mode, behaviour is non-interactive and acceptance follows settings. + ##### info `info` output fields: diff --git a/src/apps/cli/commands/p2p.ts b/src/apps/cli/commands/p2p.ts new file mode 100644 index 0000000..41013e0 --- /dev/null +++ b/src/apps/cli/commands/p2p.ts @@ -0,0 +1,149 @@ +import type { LiveSyncBaseCore } from "../../../LiveSyncBaseCore"; +import { P2P_DEFAULT_SETTINGS, SETTING_KEY_P2P_DEVICE_NAME, type EntryDoc } from "@lib/common/types"; +import type { ServiceContext } from "@lib/services/base/ServiceBase"; +import { TrysteroReplicator } from "@lib/replication/trystero/TrysteroReplicator"; + +type CLIP2PPeer = { + peerId: string; + name: string; +}; + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export function parseTimeoutSeconds(value: string, commandName: string): number { + const timeoutSec = Number(value); + if (!Number.isFinite(timeoutSec) || timeoutSec < 0) { + throw new Error(`${commandName} requires a non-negative timeout in seconds`); + } + return timeoutSec; +} + +function validateP2PSettings(core: LiveSyncBaseCore) { + const settings = core.services.setting.currentSettings(); + if (!settings.P2P_Enabled) { + throw new Error("P2P is disabled in settings (P2P_Enabled=false)"); + } + if (!settings.P2P_AppID) { + settings.P2P_AppID = P2P_DEFAULT_SETTINGS.P2P_AppID; + } + // CLI mode is non-interactive. + settings.P2P_IsHeadless = true; +} + +async function createReplicator(core: LiveSyncBaseCore): Promise { + validateP2PSettings(core); + const getSettings = () => core.services.setting.currentSettings(); + const getDB = () => core.services.database.localDatabase.localDatabase; + const getSimpleStore = () => core.services.keyValueDB.openSimpleStore("p2p-sync"); + const getDeviceName = () => + core.services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME) || core.services.vault.getVaultName(); + + const env = { + get settings() { + return getSettings(); + }, + get db() { + return getDB(); + }, + get simpleStore() { + return getSimpleStore(); + }, + get deviceName() { + return getDeviceName(); + }, + get platform() { + return core.services.API.getPlatform(); + }, + get confirm() { + return core.services.API.confirm; + }, + processReplicatedDocs: async (docs: EntryDoc[]) => { + await core.services.replication.parseSynchroniseResult(docs as any); + }, + }; + + return new TrysteroReplicator(env as any); +} + +function getSortedPeers(replicator: TrysteroReplicator): CLIP2PPeer[] { + return [...replicator.knownAdvertisements] + .map((peer) => ({ peerId: peer.peerId, name: peer.name })) + .sort((a, b) => a.peerId.localeCompare(b.peerId)); +} + +export async function collectPeers( + core: LiveSyncBaseCore, + timeoutSec: number +): Promise { + const replicator = await createReplicator(core); + await replicator.open(); + try { + await delay(timeoutSec * 1000); + return getSortedPeers(replicator); + } finally { + await replicator.close(); + } +} + +function resolvePeer(peers: CLIP2PPeer[], peerToken: string): CLIP2PPeer | undefined { + const byId = peers.find((peer) => peer.peerId === peerToken); + if (byId) { + return byId; + } + const byName = peers.filter((peer) => peer.name === peerToken); + if (byName.length > 1) { + throw new Error(`Multiple peers matched by name '${peerToken}'. Use peer-id instead.`); + } + if (byName.length === 1) { + return byName[0]; + } + return undefined; +} + +export async function syncWithPeer( + core: LiveSyncBaseCore, + peerToken: string, + timeoutSec: number +): Promise { + const replicator = await createReplicator(core); + await replicator.open(); + try { + const timeoutMs = timeoutSec * 1000; + const start = Date.now(); + let targetPeer: CLIP2PPeer | undefined; + + while (Date.now() - start <= timeoutMs) { + const peers = getSortedPeers(replicator); + targetPeer = resolvePeer(peers, peerToken); + if (targetPeer) { + break; + } + await delay(200); + } + + if (!targetPeer) { + throw new Error(`Peer '${peerToken}' was not found within ${timeoutSec} seconds`); + } + + const pullResult = await replicator.replicateFrom(targetPeer.peerId, false); + if (pullResult && "error" in pullResult && pullResult.error) { + throw pullResult.error; + } + const pushResult = (await replicator.requestSynchroniseToPeer(targetPeer.peerId)) as any; + if (!pushResult || pushResult.ok !== true) { + throw pushResult?.error ?? new Error("P2P sync failed while requesting remote sync"); + } + + return targetPeer; + } finally { + await replicator.close(); + } +} + +export async function openP2PHost(core: LiveSyncBaseCore): Promise { + const replicator = await createReplicator(core); + await replicator.open(); + return replicator; +} diff --git a/src/apps/cli/commands/p2p.unit.spec.ts b/src/apps/cli/commands/p2p.unit.spec.ts new file mode 100644 index 0000000..d0c2342 --- /dev/null +++ b/src/apps/cli/commands/p2p.unit.spec.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { parseTimeoutSeconds } from "./p2p"; + +describe("p2p command helpers", () => { + it("accepts non-negative timeout", () => { + expect(parseTimeoutSeconds("0", "p2p-peers")).toBe(0); + expect(parseTimeoutSeconds("2.5", "p2p-sync")).toBe(2.5); + }); + + it("rejects invalid timeout values", () => { + expect(() => parseTimeoutSeconds("-1", "p2p-peers")).toThrow( + "p2p-peers requires a non-negative timeout in seconds" + ); + expect(() => parseTimeoutSeconds("abc", "p2p-sync")).toThrow( + "p2p-sync requires a non-negative timeout in seconds" + ); + }); +}); diff --git a/src/apps/cli/commands/runCommand.ts b/src/apps/cli/commands/runCommand.ts index 0b68dc8..b005acb 100644 --- a/src/apps/cli/commands/runCommand.ts +++ b/src/apps/cli/commands/runCommand.ts @@ -6,6 +6,7 @@ import { DEFAULT_SETTINGS, type FilePathWithPrefix, type ObsidianLiveSyncSetting import { stripAllPrefixes } from "@lib/string_and_binary/path"; import type { CLICommandContext, CLIOptions } from "./types"; import { promptForPassphrase, readStdinAsUtf8, toArrayBuffer, toVaultRelativePath } from "./utils"; +import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p"; import { performFullScan } from "@lib/serviceFeatures/offlineScanner"; import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager"; @@ -23,6 +24,42 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext return !!result; } + if (options.command === "p2p-peers") { + if (options.commandArgs.length < 1) { + throw new Error("p2p-peers requires one argument: "); + } + const timeoutSec = parseTimeoutSeconds(options.commandArgs[0], "p2p-peers"); + console.error(`[Command] p2p-peers timeout=${timeoutSec}s`); + const peers = await collectPeers(core as any, timeoutSec); + if (peers.length > 0) { + process.stdout.write(peers.map((peer) => `[peer]\t${peer.peerId}\t${peer.name}`).join("\n") + "\n"); + } + return true; + } + + if (options.command === "p2p-sync") { + if (options.commandArgs.length < 2) { + throw new Error("p2p-sync requires two arguments: "); + } + const peerToken = options.commandArgs[0].trim(); + if (!peerToken) { + throw new Error("p2p-sync requires a non-empty "); + } + const timeoutSec = parseTimeoutSeconds(options.commandArgs[1], "p2p-sync"); + console.error(`[Command] p2p-sync peer=${peerToken} timeout=${timeoutSec}s`); + const peer = await syncWithPeer(core as any, peerToken, timeoutSec); + console.error(`[Done] P2P sync completed with ${peer.name} (${peer.peerId})`); + return true; + } + + if (options.command === "p2p-host") { + console.error("[Command] p2p-host"); + await openP2PHost(core as any); + console.error("[Ready] P2P host is running. Press Ctrl+C to stop."); + await new Promise(() => {}); + return true; + } + if (options.command === "push") { if (options.commandArgs.length < 2) { throw new Error("push requires two arguments: "); diff --git a/src/apps/cli/commands/types.ts b/src/apps/cli/commands/types.ts index a4d7fe1..01ea118 100644 --- a/src/apps/cli/commands/types.ts +++ b/src/apps/cli/commands/types.ts @@ -4,6 +4,9 @@ import { ServiceContext } from "@lib/services/base/ServiceBase"; export type CLICommand = | "daemon" | "sync" + | "p2p-peers" + | "p2p-sync" + | "p2p-host" | "push" | "pull" | "pull-rev" @@ -36,6 +39,9 @@ export interface CLICommandContext { export const VALID_COMMANDS = new Set([ "sync", + "p2p-peers", + "p2p-sync", + "p2p-host", "push", "pull", "pull-rev", diff --git a/src/apps/cli/entrypoint.ts b/src/apps/cli/entrypoint.ts index b8a1177..8da7104 100644 --- a/src/apps/cli/entrypoint.ts +++ b/src/apps/cli/entrypoint.ts @@ -1,6 +1,12 @@ #!/usr/bin/env node +import polyfill from "node-datachannel/polyfill"; import { main } from "./main"; +for (const prop in polyfill) { + // @ts-ignore Applying polyfill to globalThis + globalThis[prop] = (polyfill as any)[prop]; +} + main().catch((error) => { console.error(`[Fatal Error]`, error); process.exit(1); diff --git a/src/apps/cli/main.ts b/src/apps/cli/main.ts index 460c90e..264d14d 100644 --- a/src/apps/cli/main.ts +++ b/src/apps/cli/main.ts @@ -27,7 +27,14 @@ import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules" import { DEFAULT_SETTINGS, LOG_LEVEL_VERBOSE, type LOG_LEVEL, type ObsidianLiveSyncSettings } from "@lib/common/types"; import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub"; import type { InjectableSettingService } from "@/lib/src/services/implements/injectable/InjectableSettingService"; -import { LOG_LEVEL_DEBUG, setGlobalLogFunction, defaultLoggerEnv, LOG_LEVEL_INFO, LOG_LEVEL_URGENT, LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger"; +import { + LOG_LEVEL_DEBUG, + setGlobalLogFunction, + defaultLoggerEnv, + LOG_LEVEL_INFO, + LOG_LEVEL_URGENT, + LOG_LEVEL_NOTICE, +} from "octagonal-wheels/common/logger"; import { runCommand } from "./commands/runCommand"; import { VALID_COMMANDS } from "./commands/types"; import type { CLICommand, CLIOptions } from "./commands/types"; @@ -36,6 +43,7 @@ import { stripAllPrefixes } from "@lib/string_and_binary/path"; const SETTINGS_FILE = ".livesync/settings.json"; defaultLoggerEnv.minLogLevel = LOG_LEVEL_DEBUG; + // DI the log again. // const recentLogEntries = reactiveSource([]); // const globalLogFunction = (message: any, level?: number, key?: string) => { @@ -65,6 +73,10 @@ Arguments: Commands: sync Run one replication cycle and exit + p2p-peers Show discovered peers as [peer] + p2p-sync + Sync with the specified peer-id or peer-name + p2p-host Start P2P host mode and wait until interrupted push Push local file into local database path pull Pull file from local database into local file pull-rev Pull file at specific revision into local file @@ -78,6 +90,9 @@ Commands: resolve Resolve conflicts by keeping and deleting others Examples: livesync-cli ./my-database sync + livesync-cli ./my-database p2p-peers 5 + livesync-cli ./my-database p2p-sync my-peer-name 15 + livesync-cli ./my-database p2p-host livesync-cli ./my-database --settings ./custom-settings.json push ./note.md folder/note.md livesync-cli ./my-database pull folder/note.md ./exports/note.md livesync-cli ./my-database pull-rev folder/note.md ./exports/note.old.md 3-abcdef @@ -213,21 +228,22 @@ export async function main() { options.command === "cat" || options.command === "cat-rev" || options.command === "ls" || + options.command === "p2p-peers" || options.command === "info" || options.command === "rm" || options.command === "resolve"; const infoLog = avoidStdoutNoise ? console.error : console.log; - if(options.debug){ + if (options.debug) { setGlobalLogFunction((msg, level) => { console.error(`[${level}] ${typeof msg === "string" ? msg : JSON.stringify(msg)}`); if (msg instanceof Error) { console.error(msg); } }); - }else{ + } else { setGlobalLogFunction((msg, level) => { // NO OP, leave it to logFunction - }) + }); } if (options.command === "init-settings") { await createDefaultSettingsFile(options); @@ -421,4 +437,6 @@ export async function main() { console.error(`[Error] Failed to start:`, error); process.exit(1); } + // To prevent unexpected hanging in webRTC connections. + process.exit(0); } diff --git a/src/apps/cli/main.unit.spec.ts b/src/apps/cli/main.unit.spec.ts index 032a2b4..8206f03 100644 --- a/src/apps/cli/main.unit.spec.ts +++ b/src/apps/cli/main.unit.spec.ts @@ -58,4 +58,31 @@ describe("CLI parseArgs", () => { expect(combined).toContain("Usage:"); expect(combined).toContain("livesync-cli [database-path]"); }); + + it("parses p2p-peers command and timeout", () => { + process.argv = ["node", "livesync-cli", "./vault", "p2p-peers", "5"]; + const parsed = parseArgs(); + + expect(parsed.databasePath).toBe("./vault"); + expect(parsed.command).toBe("p2p-peers"); + expect(parsed.commandArgs).toEqual(["5"]); + }); + + it("parses p2p-sync command with peer and timeout", () => { + process.argv = ["node", "livesync-cli", "./vault", "p2p-sync", "peer-1", "12"]; + const parsed = parseArgs(); + + expect(parsed.databasePath).toBe("./vault"); + expect(parsed.command).toBe("p2p-sync"); + expect(parsed.commandArgs).toEqual(["peer-1", "12"]); + }); + + it("parses p2p-host command", () => { + process.argv = ["node", "livesync-cli", "./vault", "p2p-host"]; + const parsed = parseArgs(); + + expect(parsed.databasePath).toBe("./vault"); + expect(parsed.command).toBe("p2p-host"); + expect(parsed.commandArgs).toEqual([]); + }); }); diff --git a/src/apps/cli/package.json b/src/apps/cli/package.json index af2b296..822c931 100644 --- a/src/apps/cli/package.json +++ b/src/apps/cli/package.json @@ -11,16 +11,20 @@ "cli": "node dist/index.cjs", "buildRun": "npm run build && npm run cli --", "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json", - "test:unit": "cd ../../.. && npx vitest run --config vitest.config.unit.ts src/apps/cli/main.unit.spec.ts src/apps/cli/commands/utils.unit.spec.ts src/apps/cli/commands/runCommand.unit.spec.ts", + "test:unit": "cd ../../.. && npx vitest run --config vitest.config.unit.ts src/apps/cli/main.unit.spec.ts src/apps/cli/commands/utils.unit.spec.ts src/apps/cli/commands/runCommand.unit.spec.ts src/apps/cli/commands/p2p.unit.spec.ts", "test:e2e:two-vaults": "bash test/test-e2e-two-vaults-with-docker-linux.sh", "test:e2e:two-vaults:common": "bash test/test-e2e-two-vaults-common.sh", "test:e2e:two-vaults:matrix": "bash test/test-e2e-two-vaults-matrix.sh", "test:e2e:push-pull": "bash test/test-push-pull-linux.sh", "test:e2e:setup-put-cat": "bash test/test-setup-put-cat-linux.sh", "test:e2e:sync-two-local": "bash test/test-sync-two-local-databases-linux.sh", + "test:e2e:p2p": "bash test/test-p2p-three-nodes-conflict-linux.sh", + "test:e2e:p2p-host": "bash test/test-p2p-host-linux.sh", + "test:e2e:p2p-sync": "bash test/test-p2p-sync-linux.sh", + "test:e2e:p2p-peers:local-relay": "bash test/test-p2p-peers-local-relay.sh", "test:e2e:mirror": "bash test/test-mirror-linux.sh", "pretest:e2e:all": "npm run build", - "test:e2e:all": " export RUN_BUILD=0 && npm run test:e2e:setup-put-cat && npm run test:e2e:push-pull && npm run test:e2e:sync-two-local && npm run test:e2e:mirror && npm run test:e2e:two-vaults" + "test:e2e:all": " export RUN_BUILD=0 && npm run test:e2e:setup-put-cat && npm run test:e2e:push-pull && npm run test:e2e:sync-two-local && npm run test:e2e:p2p && npm run test:e2e:mirror && npm run test:e2e:two-vaults && npm run test:e2e:p2p-host && npm run test:e2e:p2p" }, "dependencies": {}, "devDependencies": {} diff --git a/src/apps/cli/services/NodeKeyValueDBService.ts b/src/apps/cli/services/NodeKeyValueDBService.ts index 5799bf6..10b1626 100644 --- a/src/apps/cli/services/NodeKeyValueDBService.ts +++ b/src/apps/cli/services/NodeKeyValueDBService.ts @@ -101,9 +101,7 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase { private load() { try { const loaded = JSON.parse(nodeFs.readFileSync(this.filePath, "utf-8")) as Record; - this.data = new Map( - Object.entries(loaded).map(([key, value]) => [key, deserializeFromNodeKV(value)]) - ); + this.data = new Map(Object.entries(loaded).map(([key, value]) => [key, deserializeFromNodeKV(value)])); } catch { this.data = new Map(); } diff --git a/src/apps/cli/test/test-helpers.sh b/src/apps/cli/test/test-helpers.sh index a508f95..0e0e236 100644 --- a/src/apps/cli/test/test-helpers.sh +++ b/src/apps/cli/test/test-helpers.sh @@ -211,6 +211,57 @@ fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2), "utf-8"); NODE } +cli_test_apply_p2p_settings() { + local settings_file="$1" + local room_id="$2" + local passphrase="$3" + local app_id="${4:-self-hosted-livesync-cli-tests}" + local relays="${5:-ws://localhost:4000/}" + local auto_accept="${6:-~.*}" + SETTINGS_FILE="$settings_file" \ + P2P_ROOM_ID="$room_id" \ + P2P_PASSPHRASE="$passphrase" \ + P2P_APP_ID="$app_id" \ + P2P_RELAYS="$relays" \ + P2P_AUTO_ACCEPT="$auto_accept" \ + node <<'NODE' +const fs = require("node:fs"); +const settingsPath = process.env.SETTINGS_FILE; +const data = JSON.parse(fs.readFileSync(settingsPath, "utf-8")); + +data.P2P_Enabled = true; +data.P2P_AutoStart = false; +data.P2P_AutoBroadcast = false; +data.P2P_AppID = process.env.P2P_APP_ID; +data.P2P_roomID = process.env.P2P_ROOM_ID; +data.P2P_passphrase = process.env.P2P_PASSPHRASE; +data.P2P_relays = process.env.P2P_RELAYS; +data.P2P_AutoAcceptingPeers = process.env.P2P_AUTO_ACCEPT; +data.P2P_AutoDenyingPeers = ""; +data.P2P_IsHeadless = true; +data.isConfigured = true; + +fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2), "utf-8"); +NODE +} + +cli_test_is_local_p2p_relay() { + local relay_url="$1" + [[ "$relay_url" == "ws://localhost:4000" || "$relay_url" == "ws://localhost:4000/" ]] +} + +cli_test_stop_p2p_relay() { + bash "$CLI_DIR/util/p2p-stop.sh" >/dev/null 2>&1 || true +} + +cli_test_start_p2p_relay() { + echo "[INFO] stopping leftover P2P relay container if present" + cli_test_stop_p2p_relay + + echo "[INFO] starting local P2P relay container" + bash "$CLI_DIR/util/p2p-start.sh" +} + cli_test_stop_couchdb() { bash "$CLI_DIR/util/couchdb-stop.sh" >/dev/null 2>&1 || true } diff --git a/src/apps/cli/test/test-p2p-host-linux.sh b/src/apps/cli/test/test-p2p-host-linux.sh new file mode 100644 index 0000000..cb31d85 --- /dev/null +++ b/src/apps/cli/test/test-p2p-host-linux.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail +# This test should be run with P2P client, please refer to the test-p2p-three-nodes-conflict-linux.sh test for more details. + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +CLI_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" +cd "$CLI_DIR" +source "$SCRIPT_DIR/test-helpers.sh" +display_test_info + +RUN_BUILD="${RUN_BUILD:-1}" +VERBOSE_TEST_LOGGING="${VERBOSE_TEST_LOGGING:-0}" +KEEP_TEST_DATA="${KEEP_TEST_DATA:-0}" + +RELAY="${RELAY:-ws://localhost:4000/}" +USE_INTERNAL_RELAY="${USE_INTERNAL_RELAY:-1}" +ROOM_ID="${ROOM_ID:-1}" +PASSPHRASE="${PASSPHRASE:-test}" +APP_ID="${APP_ID:-self-hosted-livesync-cli-tests}" + +cli_test_init_cli_cmd + +if [[ "$RUN_BUILD" == "1" ]]; then + echo "[INFO] building CLI" + npm run build +fi + +WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/livesync-cli-p2p-host.XXXXXX")" +VAULT="$WORK_DIR/vault-host" +SETTINGS="$WORK_DIR/settings-host.json" +mkdir -p "$VAULT" + +cleanup() { + local exit_code=$? + if [[ "${P2P_RELAY_STARTED:-0}" == "1" ]]; then + cli_test_stop_p2p_relay + fi + + if [[ "$KEEP_TEST_DATA" != "1" ]]; then + rm -rf "$WORK_DIR" + else + echo "[INFO] KEEP_TEST_DATA=1, preserving artefacts at $WORK_DIR" + fi + exit "$exit_code" +} +trap cleanup EXIT + +if [[ "$USE_INTERNAL_RELAY" == "1" ]]; then + if cli_test_is_local_p2p_relay "$RELAY"; then + cli_test_start_p2p_relay + P2P_RELAY_STARTED=1 + else + echo "[INFO] USE_INTERNAL_RELAY=1 but RELAY is not local ($RELAY), skipping local relay startup" + fi +fi + +echo "[INFO] preparing settings" +echo "[INFO] relay=$RELAY room=$ROOM_ID app=$APP_ID" +cli_test_init_settings_file "$SETTINGS" +cli_test_apply_p2p_settings "$SETTINGS" "$ROOM_ID" "$PASSPHRASE" "$APP_ID" "$RELAY" + +echo "[CASE] start p2p-host" +echo "[INFO] press Ctrl+C to stop" +run_cli "$VAULT" --settings "$SETTINGS" p2p-host diff --git a/src/apps/cli/test/test-p2p-peers-local-relay.sh b/src/apps/cli/test/test-p2p-peers-local-relay.sh new file mode 100644 index 0000000..214a2ad --- /dev/null +++ b/src/apps/cli/test/test-p2p-peers-local-relay.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +CLI_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" +cd "$CLI_DIR" + +source "$SCRIPT_DIR/test-helpers.sh" + +RUN_BUILD="${RUN_BUILD:-0}" +KEEP_TEST_DATA="${KEEP_TEST_DATA:-0}" +RELAY="${RELAY:-ws://localhost:7777}" +ROOM_ID="${ROOM_ID:-1}" +PASSPHRASE="${PASSPHRASE:-test}" +TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-8}" +DEBUG_FLAG="${DEBUG_FLAG:--d}" + +if [[ "$RUN_BUILD" == "1" ]]; then + echo "[INFO] building CLI" + npm run build +fi + +WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/livesync-cli-p2p-peers-local-relay.XXXXXX")" +VAULT="$WORK_DIR/vault" +SETTINGS="$WORK_DIR/settings.json" +mkdir -p "$VAULT" + +cleanup() { + local exit_code=$? + if [[ "$KEEP_TEST_DATA" != "1" ]]; then + rm -rf "$WORK_DIR" + else + echo "[INFO] KEEP_TEST_DATA=1, preserving artefacts at $WORK_DIR" + fi + exit "$exit_code" +} +trap cleanup EXIT + +cli_test_init_cli_cmd + +echo "[INFO] creating settings at $SETTINGS" +run_cli init-settings --force "$SETTINGS" >/dev/null + +SETTINGS_FILE="$SETTINGS" \ +P2P_ROOM_ID="$ROOM_ID" \ +P2P_PASSPHRASE="$PASSPHRASE" \ +P2P_RELAYS="$RELAY" \ +node <<'NODE' +const fs = require("node:fs"); +const settingsPath = process.env.SETTINGS_FILE; +const data = JSON.parse(fs.readFileSync(settingsPath, "utf-8")); + +data.P2P_Enabled = true; +data.P2P_AutoStart = false; +data.P2P_AutoBroadcast = false; +data.P2P_roomID = process.env.P2P_ROOM_ID; +data.P2P_passphrase = process.env.P2P_PASSPHRASE; +data.P2P_relays = process.env.P2P_RELAYS; +data.P2P_AutoAcceptingPeers = "~.*"; +data.P2P_AutoDenyingPeers = ""; +data.P2P_IsHeadless = true; +data.isConfigured = true; + +fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2), "utf-8"); +NODE + +echo "[INFO] relay=$RELAY room=$ROOM_ID timeout=${TIMEOUT_SECONDS}s" +echo "[INFO] running p2p-peers" + +set +e +OUTPUT="$(run_cli "$DEBUG_FLAG" "$VAULT" --settings "$SETTINGS" p2p-peers "$TIMEOUT_SECONDS" 2>&1)" +EXIT_CODE=$? +set -e + +echo "$OUTPUT" + +if [[ "$EXIT_CODE" -ne 0 ]]; then + echo "[FAIL] p2p-peers exited with code $EXIT_CODE" >&2 + exit "$EXIT_CODE" +fi + +if [[ -z "$OUTPUT" ]]; then + echo "[WARN] command completed but output was empty" +fi + +echo "[PASS] p2p-peers finished" diff --git a/src/apps/cli/test/test-p2p-sync-linux.sh b/src/apps/cli/test/test-p2p-sync-linux.sh new file mode 100644 index 0000000..3960de1 --- /dev/null +++ b/src/apps/cli/test/test-p2p-sync-linux.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# This test should be run with P2P client, please refer to the test-p2p-three-nodes-conflict-linux.sh test for more details. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +CLI_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" +cd "$CLI_DIR" +source "$SCRIPT_DIR/test-helpers.sh" +display_test_info + +RUN_BUILD="${RUN_BUILD:-1}" +VERBOSE_TEST_LOGGING="${VERBOSE_TEST_LOGGING:-0}" +KEEP_TEST_DATA="${KEEP_TEST_DATA:-0}" + +RELAY="${RELAY:-ws://localhost:4000/}" +USE_INTERNAL_RELAY="${USE_INTERNAL_RELAY:-1}" +ROOM_ID="${ROOM_ID:-1}" +PASSPHRASE="${PASSPHRASE:-test}" +APP_ID="${APP_ID:-self-hosted-livesync-cli-tests}" +PEERS_TIMEOUT="${PEERS_TIMEOUT:-12}" +SYNC_TIMEOUT="${SYNC_TIMEOUT:-15}" +TARGET_PEER="${TARGET_PEER:-}" + +cli_test_init_cli_cmd + +if [[ "$RUN_BUILD" == "1" ]]; then + echo "[INFO] building CLI" + npm run build +fi + +WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/livesync-cli-p2p-sync.XXXXXX")" +VAULT="$WORK_DIR/vault-sync" +SETTINGS="$WORK_DIR/settings-sync.json" +mkdir -p "$VAULT" + +cleanup() { + local exit_code=$? + if [[ "${P2P_RELAY_STARTED:-0}" == "1" ]]; then + cli_test_stop_p2p_relay + fi + + if [[ "$KEEP_TEST_DATA" != "1" ]]; then + rm -rf "$WORK_DIR" + else + echo "[INFO] KEEP_TEST_DATA=1, preserving artefacts at $WORK_DIR" + fi + exit "$exit_code" +} +trap cleanup EXIT + +if [[ "$USE_INTERNAL_RELAY" == "1" ]]; then + if cli_test_is_local_p2p_relay "$RELAY"; then + cli_test_start_p2p_relay + P2P_RELAY_STARTED=1 + else + echo "[INFO] USE_INTERNAL_RELAY=1 but RELAY is not local ($RELAY), skipping local relay startup" + fi +fi + +echo "[INFO] preparing settings" +echo "[INFO] relay=$RELAY room=$ROOM_ID app=$APP_ID" +cli_test_init_settings_file "$SETTINGS" +cli_test_apply_p2p_settings "$SETTINGS" "$ROOM_ID" "$PASSPHRASE" "$APP_ID" "$RELAY" + +echo "[CASE] discover peers" +PEER_LINES="$(run_cli "$VAULT" --settings "$SETTINGS" p2p-peers "$PEERS_TIMEOUT")" +if [[ -z "$PEER_LINES" ]]; then + echo "[FAIL] p2p-peers returned empty output" >&2 + exit 1 +fi + +if ! awk -F $'\t' 'NF>=3 && $1=="[peer]" { found=1 } END { exit(found ? 0 : 1) }' <<< "$PEER_LINES"; then + echo "[FAIL] p2p-peers output must include [peer]" >&2 + echo "$PEER_LINES" >&2 + exit 1 +fi + +SELECTED_PEER_ID="" +SELECTED_PEER_NAME="" + +if [[ -n "$TARGET_PEER" ]]; then + while IFS=$'\t' read -r marker peer_id peer_name _; do + if [[ "$marker" != "[peer]" ]]; then + continue + fi + if [[ "$peer_id" == "$TARGET_PEER" || "$peer_name" == "$TARGET_PEER" ]]; then + SELECTED_PEER_ID="$peer_id" + SELECTED_PEER_NAME="$peer_name" + break + fi + done <<< "$PEER_LINES" + + if [[ -z "$SELECTED_PEER_ID" ]]; then + echo "[FAIL] TARGET_PEER=$TARGET_PEER was not found" >&2 + echo "$PEER_LINES" >&2 + exit 1 + fi +else + SELECTED_PEER_ID="$(awk -F $'\t' 'NF>=3 && $1=="[peer]" {print $2; exit}' <<< "$PEER_LINES")" + SELECTED_PEER_NAME="$(awk -F $'\t' 'NF>=3 && $1=="[peer]" {print $3; exit}' <<< "$PEER_LINES")" +fi + +if [[ -z "$SELECTED_PEER_ID" ]]; then + echo "[FAIL] could not extract peer-id from p2p-peers output" >&2 + echo "$PEER_LINES" >&2 + exit 1 +fi + +echo "[PASS] selected peer: ${SELECTED_PEER_ID} (${SELECTED_PEER_NAME:-unknown})" + +echo "[CASE] run p2p-sync" +run_cli "$VAULT" --settings "$SETTINGS" p2p-sync "$SELECTED_PEER_ID" "$SYNC_TIMEOUT" >/dev/null + +echo "[PASS] p2p-sync completed" diff --git a/src/apps/cli/test/test-p2p-three-nodes-conflict-linux.sh b/src/apps/cli/test/test-p2p-three-nodes-conflict-linux.sh new file mode 100644 index 0000000..a4ce4af --- /dev/null +++ b/src/apps/cli/test/test-p2p-three-nodes-conflict-linux.sh @@ -0,0 +1,242 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +CLI_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" +cd "$CLI_DIR" +source "$SCRIPT_DIR/test-helpers.sh" +display_test_info + +RUN_BUILD="${RUN_BUILD:-1}" +KEEP_TEST_DATA="${KEEP_TEST_DATA:-0}" +VERBOSE_TEST_LOGGING="${VERBOSE_TEST_LOGGING:-0}" + +RELAY="${RELAY:-ws://localhost:4000/}" +USE_INTERNAL_RELAY="${USE_INTERNAL_RELAY:-1}" +ROOM_ID_PREFIX="${ROOM_ID_PREFIX:-p2p-room}" +PASSPHRASE_PREFIX="${PASSPHRASE_PREFIX:-p2p-pass}" +APP_ID="${APP_ID:-self-hosted-livesync-cli-tests}" +PEERS_TIMEOUT="${PEERS_TIMEOUT:-10}" +SYNC_TIMEOUT="${SYNC_TIMEOUT:-15}" + +ROOM_ID="${ROOM_ID_PREFIX}-$(date +%s)-$RANDOM-$RANDOM" +PASSPHRASE="${PASSPHRASE_PREFIX}-$(date +%s)-$RANDOM-$RANDOM" + +cli_test_init_cli_cmd + +if [[ "$RUN_BUILD" == "1" ]]; then + echo "[INFO] building CLI" + npm run build +fi + +WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/livesync-cli-p2p-3nodes.XXXXXX")" +VAULT_A="$WORK_DIR/vault-a" +VAULT_B="$WORK_DIR/vault-b" +VAULT_C="$WORK_DIR/vault-c" +SETTINGS_A="$WORK_DIR/settings-a.json" +SETTINGS_B="$WORK_DIR/settings-b.json" +SETTINGS_C="$WORK_DIR/settings-c.json" +HOST_LOG="$WORK_DIR/p2p-host.log" + +mkdir -p "$VAULT_A" "$VAULT_B" "$VAULT_C" + +cleanup() { + local exit_code=$? + if [[ -n "${HOST_PID:-}" ]] && kill -0 "$HOST_PID" >/dev/null 2>&1; then + kill -TERM "$HOST_PID" >/dev/null 2>&1 || true + wait "$HOST_PID" >/dev/null 2>&1 || true + fi + + if [[ "${P2P_RELAY_STARTED:-0}" == "1" ]]; then + cli_test_stop_p2p_relay + fi + + if [[ "$KEEP_TEST_DATA" != "1" ]]; then + rm -rf "$WORK_DIR" + else + echo "[INFO] KEEP_TEST_DATA=1, preserving artefacts at $WORK_DIR" + fi + + exit "$exit_code" +} +trap cleanup EXIT + +if [[ "$USE_INTERNAL_RELAY" == "1" ]]; then + if cli_test_is_local_p2p_relay "$RELAY"; then + cli_test_start_p2p_relay + P2P_RELAY_STARTED=1 + else + echo "[INFO] USE_INTERNAL_RELAY=1 but RELAY is not local ($RELAY), skipping local relay startup" + fi +fi + +run_cli_a() { + run_cli "$VAULT_A" --settings "$SETTINGS_A" "$@" +} + +run_cli_b() { + run_cli "$VAULT_B" --settings "$SETTINGS_B" "$@" +} + +run_cli_c() { + run_cli "$VAULT_C" --settings "$SETTINGS_C" "$@" +} + +echo "[INFO] preparing settings" +echo "[INFO] relay=$RELAY room=$ROOM_ID app=$APP_ID" +cli_test_init_settings_file "$SETTINGS_A" +cli_test_init_settings_file "$SETTINGS_B" +cli_test_init_settings_file "$SETTINGS_C" +cli_test_apply_p2p_settings "$SETTINGS_A" "$ROOM_ID" "$PASSPHRASE" "$APP_ID" "$RELAY" +cli_test_apply_p2p_settings "$SETTINGS_B" "$ROOM_ID" "$PASSPHRASE" "$APP_ID" "$RELAY" +cli_test_apply_p2p_settings "$SETTINGS_C" "$ROOM_ID" "$PASSPHRASE" "$APP_ID" "$RELAY" + +echo "[CASE] start p2p-host on A" +run_cli_a p2p-host >"$HOST_LOG" 2>&1 & +HOST_PID=$! + +for _ in 1 2 3 4 5 6 7 8 9 10; do + echo "[INFO] waiting for p2p-host to start..." + if grep -Fq "P2P host is running" "$HOST_LOG"; then + break + fi + sleep 1 +done +if ! grep -Fq "P2P host is running" "$HOST_LOG"; then + echo "[FAIL] p2p-host did not become ready" >&2 + cat "$HOST_LOG" >&2 + exit 1 +fi +echo "[PASS] p2p-host started" + +echo "[CASE] discover host peer from B" +PEERS_FROM_B="$(run_cli_b p2p-peers "$PEERS_TIMEOUT")" +HOST_PEER_ID="$(awk -F $'\t' 'NF>=3 && $1=="[peer]" {print $2; exit}' <<< "$PEERS_FROM_B")" +if [[ -z "$HOST_PEER_ID" ]]; then + echo "[FAIL] B could not find host peer" >&2 + echo "$PEERS_FROM_B" >&2 + exit 1 +fi +echo "[PASS] B discovered host peer: $HOST_PEER_ID" + +echo "[CASE] discover host peer from C" +PEERS_FROM_C="$(run_cli_c p2p-peers "$PEERS_TIMEOUT")" +HOST_PEER_ID_FROM_C="$(awk -F $'\t' 'NF>=3 && $1=="[peer]" {print $2; exit}' <<< "$PEERS_FROM_C")" +if [[ -z "$HOST_PEER_ID_FROM_C" ]]; then + echo "[FAIL] C could not find host peer" >&2 + echo "$PEERS_FROM_C" >&2 + exit 1 +fi +echo "[PASS] C discovered host peer: $HOST_PEER_ID_FROM_C" + +TARGET_PATH="p2p/conflicted-from-two-clients.txt" + +echo "[CASE] B creates file and syncs" +printf 'from-client-b-v1\n' | run_cli_b put "$TARGET_PATH" >/dev/null +run_cli_b p2p-sync "$HOST_PEER_ID" "$SYNC_TIMEOUT" >/dev/null + +echo "[CASE] C syncs and can see B file" +run_cli_c p2p-sync "$HOST_PEER_ID_FROM_C" "$SYNC_TIMEOUT" >/dev/null +VISIBLE_ON_C="" +for _ in 1 2 3 4 5; do + if VISIBLE_ON_C="$(run_cli_c cat "$TARGET_PATH" 2>/dev/null | cli_test_sanitise_cat_stdout)"; then + if [[ "$VISIBLE_ON_C" == "from-client-b-v1" ]]; then + break + fi + fi + run_cli_c p2p-sync "$HOST_PEER_ID_FROM_C" "$SYNC_TIMEOUT" >/dev/null + sleep 1 +done +cli_test_assert_equal "from-client-b-v1" "$VISIBLE_ON_C" "C should see file created by B" + +echo "[CASE] B and C modify file independently" +printf 'from-client-b-v2\n' | run_cli_b put "$TARGET_PATH" >/dev/null +printf 'from-client-c-v2\n' | run_cli_c put "$TARGET_PATH" >/dev/null + +echo "[CASE] B and C sync to host concurrently" +set +e +run_cli_b p2p-sync "$HOST_PEER_ID" "$SYNC_TIMEOUT" >/dev/null & +SYNC_B_PID=$! +run_cli_c p2p-sync "$HOST_PEER_ID_FROM_C" "$SYNC_TIMEOUT" >/dev/null & +SYNC_C_PID=$! +wait "$SYNC_B_PID" +SYNC_B_EXIT=$? +wait "$SYNC_C_PID" +SYNC_C_EXIT=$? +set -e +if [[ "$SYNC_B_EXIT" -ne 0 || "$SYNC_C_EXIT" -ne 0 ]]; then + echo "[FAIL] concurrent sync failed: B=$SYNC_B_EXIT C=$SYNC_C_EXIT" >&2 + exit 1 +fi + +echo "[CASE] sync back to clients" +run_cli_b p2p-sync "$HOST_PEER_ID" "$SYNC_TIMEOUT" >/dev/null +run_cli_c p2p-sync "$HOST_PEER_ID_FROM_C" "$SYNC_TIMEOUT" >/dev/null + +echo "[CASE] B info shows conflict" +INFO_JSON_B_BEFORE="$(run_cli_b info "$TARGET_PATH")" +CONFLICTS_B_BEFORE="$(printf '%s' "$INFO_JSON_B_BEFORE" | cli_test_json_string_field_from_stdin conflicts)" +KEEP_REV_B="$(printf '%s' "$INFO_JSON_B_BEFORE" | cli_test_json_string_field_from_stdin revision)" +if [[ "$CONFLICTS_B_BEFORE" == "N/A" || -z "$CONFLICTS_B_BEFORE" ]]; then + echo "[FAIL] expected conflicts on B after two-client sync" >&2 + echo "$INFO_JSON_B_BEFORE" >&2 + exit 1 +fi +if [[ -z "$KEEP_REV_B" ]]; then + echo "[FAIL] could not read current revision on B for resolve" >&2 + echo "$INFO_JSON_B_BEFORE" >&2 + exit 1 +fi +echo "[PASS] conflict detected on B" + +echo "[CASE] C info shows conflict" +INFO_JSON_C_BEFORE="$(run_cli_c info "$TARGET_PATH")" +CONFLICTS_C_BEFORE="$(printf '%s' "$INFO_JSON_C_BEFORE" | cli_test_json_string_field_from_stdin conflicts)" +KEEP_REV_C="$(printf '%s' "$INFO_JSON_C_BEFORE" | cli_test_json_string_field_from_stdin revision)" +if [[ "$CONFLICTS_C_BEFORE" == "N/A" || -z "$CONFLICTS_C_BEFORE" ]]; then + echo "[FAIL] expected conflicts on C after two-client sync" >&2 + echo "$INFO_JSON_C_BEFORE" >&2 + exit 1 +fi +if [[ -z "$KEEP_REV_C" ]]; then + echo "[FAIL] could not read current revision on C for resolve" >&2 + echo "$INFO_JSON_C_BEFORE" >&2 + exit 1 +fi +echo "[PASS] conflict detected on C" + +echo "[CASE] resolve conflict on B and C" +run_cli_b resolve "$TARGET_PATH" "$KEEP_REV_B" >/dev/null +run_cli_c resolve "$TARGET_PATH" "$KEEP_REV_C" >/dev/null + +INFO_JSON_B_AFTER="$(run_cli_b info "$TARGET_PATH")" +CONFLICTS_B_AFTER="$(printf '%s' "$INFO_JSON_B_AFTER" | cli_test_json_string_field_from_stdin conflicts)" +if [[ "$CONFLICTS_B_AFTER" != "N/A" ]]; then + echo "[FAIL] conflict still remains on B after resolve" >&2 + echo "$INFO_JSON_B_AFTER" >&2 + exit 1 +fi + +INFO_JSON_C_AFTER="$(run_cli_c info "$TARGET_PATH")" +CONFLICTS_C_AFTER="$(printf '%s' "$INFO_JSON_C_AFTER" | cli_test_json_string_field_from_stdin conflicts)" +if [[ "$CONFLICTS_C_AFTER" != "N/A" ]]; then + echo "[FAIL] conflict still remains on C after resolve" >&2 + echo "$INFO_JSON_C_AFTER" >&2 + exit 1 +fi + +FINAL_CONTENT_B="$(run_cli_b cat "$TARGET_PATH" | cli_test_sanitise_cat_stdout)" +FINAL_CONTENT_C="$(run_cli_c cat "$TARGET_PATH" | cli_test_sanitise_cat_stdout)" +if [[ "$FINAL_CONTENT_B" != "from-client-b-v2" && "$FINAL_CONTENT_B" != "from-client-c-v2" ]]; then + echo "[FAIL] unexpected final content on B after resolve" >&2 + echo "[FAIL] final content on B: $FINAL_CONTENT_B" >&2 + exit 1 +fi +if [[ "$FINAL_CONTENT_C" != "from-client-b-v2" && "$FINAL_CONTENT_C" != "from-client-c-v2" ]]; then + echo "[FAIL] unexpected final content on C after resolve" >&2 + echo "[FAIL] final content on C: $FINAL_CONTENT_C" >&2 + exit 1 +fi + +echo "[PASS] conflicts resolved on B and C" +echo "[PASS] all 3-node P2P conflict scenarios passed" diff --git a/src/apps/cli/util/p2p-init.sh b/src/apps/cli/util/p2p-init.sh new file mode 100755 index 0000000..dd865c9 --- /dev/null +++ b/src/apps/cli/util/p2p-init.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo "P2P Init - No additional initialization required." \ No newline at end of file diff --git a/src/apps/cli/util/p2p-start.sh b/src/apps/cli/util/p2p-start.sh new file mode 100755 index 0000000..90b3e3e --- /dev/null +++ b/src/apps/cli/util/p2p-start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker run -d --name relay-test -p 4000:8080 scsibug/nostr-rs-relay:latest diff --git a/src/apps/cli/util/p2p-stop.sh b/src/apps/cli/util/p2p-stop.sh new file mode 100755 index 0000000..c83c8c9 --- /dev/null +++ b/src/apps/cli/util/p2p-stop.sh @@ -0,0 +1,3 @@ +#!/bin/bash +docker stop relay-test +docker rm relay-test \ No newline at end of file diff --git a/src/apps/cli/vite.config.ts b/src/apps/cli/vite.config.ts index d3bdea0..77d8591 100644 --- a/src/apps/cli/vite.config.ts +++ b/src/apps/cli/vite.config.ts @@ -5,7 +5,16 @@ import { readFileSync } from "node:fs"; const packageJson = JSON.parse(readFileSync("../../../package.json", "utf-8")); const manifestJson = JSON.parse(readFileSync("../../../manifest.json", "utf-8")); // https://vite.dev/config/ -const defaultExternal = ["obsidian", "electron", "crypto", "pouchdb-adapter-leveldb", "commander", "punycode"]; +const defaultExternal = [ + "obsidian", + "electron", + "crypto", + "pouchdb-adapter-leveldb", + "commander", + "punycode", + "node-datachannel", + "node-datachannel/polyfill", +]; export default defineConfig({ plugins: [svelte()], resolve: { @@ -43,6 +52,7 @@ export default defineConfig({ if (id === "fs" || id === "fs/promises" || id === "path" || id === "crypto" || id === "worker_threads") return true; if (id.startsWith("pouchdb-")) return true; + if (id.startsWith("node-datachannel")) return true; if (id.startsWith("node:")) return true; return false; }, diff --git a/src/lib b/src/lib index f77404c..90da1d4 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit f77404c926e06e1320984158680c19b1541f1655 +Subproject commit 90da1d4fb414f08d7ec12a8585bbc111566fbe06 diff --git a/updates.md b/updates.md index e218314..662c8a0 100644 --- a/updates.md +++ b/updates.md @@ -5,9 +5,11 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid ## -- unreleased -- -### New features +### CLI new features - `mirror` command has been added to the CLI. This command is intended to mirror the storage to the local database. +- `p2p-sync`, `p2p-peers`, and `p2p-host` commands have been added to the CLI. These commands are intended for P2P synchronisation. + - Yes, no more need for a [LiveSync PeerServer](https://github.com/vrtmrz/livesync-serverpeer) for virtual environments! The CLI can handle it by itself. ## 0.25.52-patched-1