diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0b53eb8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = 4 + +# specifically for YAML +[{yml, yaml}] +indent_style = space diff --git a/.gitignore b/.gitignore index 8924c30..6fbbf6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,17 @@ -node_modules/ -build/ -config.toml \ No newline at end of file +.parcel-cache/ +**/.yarn/ +**/node_modules/ +config.toml + +# for now +cvmts/attic + +/dist +**/dist/ + +# Guac-rs +cvm-rs/target +cvm-rs/index.node + +# geolite shit +**/geoip/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..925776f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "collab-vm-1.2-binary-protocol"] + path = collab-vm-1.2-binary-protocol + url = https://github.com/computernewb/collab-vm-1.2-binary-protocol diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 9cf9495..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1ba211f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +dist +*.md +**/package.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..d41a549 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,20 @@ +{ + "arrowParens": "always", + "bracketSameLine": false, + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": true, + "printWidth": 200, + "proseWrap": "preserve", + "quoteProps": "consistent", + "requirePragma": false, + "semi": true, + "singleAttributePerLine": false, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "none", + "useTabs": true, + "vueIndentScriptAndStyle": false +} diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..684a827 --- /dev/null +++ b/Justfile @@ -0,0 +1,7 @@ +all: + yarn workspace @cvmts/cvm-rs run build + yarn workspace @cvmts/collab-vm-1.2-binary-protocol run build + yarn workspace @cvmts/cvmts run build + +pkg: + yarn diff --git a/README.md b/README.md index ea50fa6..0eeda4b 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,35 @@ # CollabVM1.ts This is a drop-in replacement for the dying CollabVM 1.2.11. Currently in beta -## Running -1. Copy config.example.toml to config.toml, and fill out fields -2. Install dependencies: `npm i` -3. Build it: `npm run build` -4. Run it: `npm run serve` +## Compatibility -## FAQ -### When I try to access the admin panel, the server crashes! -The server does not support the admin panel. Instead, there is a configuration file you can edit named config.toml. -### Why only QEMU? Why not VMWare, VirtualBox, etc.? -This server was written very quickly to replace CollabVM Server 1.2.11, and so only QEMU support exists. There are plans to support VMWare when CollabVM Server 3 releases. -### What platforms can this be run on? -If it can run a relatively new version of Node and QEMU, then you can run this. This means modern Linux distributions, modern macOS versions and Windows 10 and above. -### When the VM shuts off, instead of restarting, it freezes. -This has been fixed already, you are running a copy of the code before February 11th, 2023. +The CollabVM server will run on any Operating System that can run Node.JS and Rust. This means modern linux distributions and Windows versions. + +We do not recommend or support running CollabVM Server on Windows due to very poor support for QEMU on that platform. + +## Dependencies + +The CollabVM server requires the following to be installed on your server: + +1. Node.js (obviously) +2. QEMU (Unless you just want to use a VNC Connection as your VM) +3. A Rust toolchain (e.g: [rustup](https://rustup.rs)) +4. NASM assembler + +### Installing dependencies on Arch + +1. Install dependencies: `sudo pacman --needed --noconfirm -Sy nodejs nasm rust` +2. Enable corepack: `sudo corepack enable` + +### Installing dependencies on Debian + +TODO + +## Running + +**TODO**: These instructions are not finished for the refactor branch. + +1. Copy config.example.toml to config.toml, and fill out fields +2. Install dependencies: `yarn` +3. Build it: `yarn build` +4. Run it: `yarn serve` diff --git a/collab-vm-1.2-binary-protocol b/collab-vm-1.2-binary-protocol new file mode 160000 index 0000000..cfe9acc --- /dev/null +++ b/collab-vm-1.2-binary-protocol @@ -0,0 +1 @@ +Subproject commit cfe9acc60b87ab26cf8612398c734c8caad426b8 diff --git a/config.example.toml b/config.example.toml index 78ab4d1..e7f4f6c 100644 --- a/config.example.toml +++ b/config.example.toml @@ -10,8 +10,20 @@ proxyAllowedIps = ["127.0.0.1"] origin = false # Origins to accept connections from. originAllowedDomains = ["computernewb.com"] -# Maximum amount of active connections allowed from the same IP. -maxConnections = 3 + +[geoip] +# Enables support for showing country flags next to usernames. +enabled = false +# Directory to store and load GeoIP databases from. +directory = "geoip/" +# MaxMind license key and account ID (https://www.maxmind.com/en/accounts/current/license-key) +accountID = "" +licenseKey = "" + +[tcp] +enabled = false +host = "0.0.0.0" +port = 6014 [auth] enabled = false @@ -25,6 +37,9 @@ callForReset = false vote = true [vm] +type = "qemu" + +[qemu] qemuArgs = "qemu-system-x86_64" vncPort = 5900 snapshots = true @@ -34,10 +49,20 @@ snapshots = true # Comment out qmpSockDir if you're using Windows. qmpSockDir = "/tmp/" +[vncvm] +vncHost = "127.0.0.1" +vncPort = 5900 +# startCmd = "" +# stopCmd = "" +# rebootCmd = "" +# restoreCmd = "" + [collabvm] node = "acoolvm" displayname = "A Really Cool CollabVM Instance" motd = "welcome!" +# Maximum amount of active connections allowed from the same IP. +maxConnections = 3 # Command used to ban an IP. # Use $IP to specify an ip and (optionally) use $NAME to specify a username bancmd = "iptables -A INPUT -s $IP -j REJECT" diff --git a/cvm-rs/Cargo.lock b/cvm-rs/Cargo.lock new file mode 100644 index 0000000..00d2143 --- /dev/null +++ b/cvm-rs/Cargo.lock @@ -0,0 +1,350 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "cc" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "cvm-rs" +version = "0.1.0" +dependencies = [ + "neon", + "once_cell", + "tokio", + "turbojpeg-sys", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "neon" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d75440242411c87dc39847b0e33e961ec1f10326a9d8ecf9c1ea64a3b3c13dc" +dependencies = [ + "getrandom", + "libloading", + "neon-macros", + "once_cell", + "semver", + "send_wrapper", + "smallvec", +] + +[[package]] +name = "neon-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6813fde79b646e47e7ad75f480aa80ef76a5d9599e2717407961531169ee38b" +dependencies = [ + "quote", + "syn", + "syn-mid", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "proc-macro2" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-mid" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5dc35bb08dd1ca3dfb09dce91fd2d13294d6711c88897d9a9d60acf39bce049" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "num_cpus", + "pin-project-lite", +] + +[[package]] +name = "turbojpeg-sys" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa6daade3b979fb7454cce5ebcb9772ce7a1cf476ea27ed20ed06e13d9bc983" +dependencies = [ + "anyhow", + "cmake", + "libc", + "pkg-config", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/cvm-rs/Cargo.toml b/cvm-rs/Cargo.toml new file mode 100644 index 0000000..3b78185 --- /dev/null +++ b/cvm-rs/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "cvm-rs" +description = "Rust utility library for cvmts. Runs all the high performance code" +version = "0.1.0" +edition = "2021" +exclude = ["index.node"] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +neon = "1" + +# Required for JPEG +once_cell = "1.19.0" +tokio = { version = "1.38.0", features = [ "rt", "rt-multi-thread" ] } +turbojpeg-sys = "1.0.0" diff --git a/cvm-rs/index.d.ts b/cvm-rs/index.d.ts new file mode 100644 index 0000000..9e664f2 --- /dev/null +++ b/cvm-rs/index.d.ts @@ -0,0 +1,84 @@ +// + +// Guacamole Codec +export function guacDecode(input: string): string[]; +export function guacEncode(...items: string[]): string; + +interface JpegInputArgs { + width: number, + height: number, + stride: number, // The width of your input framebuffer OR your image width (if encoding a full image) + buffer: Buffer + + // TODO: Allow different formats, or export a boxed ffi object which can store a format + // (i.e: new JpegEncoder(FORMAT_xxx)). +} + +/// Performs JPEG encoding. +export function jpegEncode(input: JpegInputArgs) : Promise; + +// TODO: Version that can downscale? + + +/* remoting API? + +js side api: + + class RemotingClient extends EventEmitter { + constructor(uri: string) + + Connect(): Promise - connects to server. + + Disconnect(): void - disconnects from a server. + + get FullScreen(): Buffer - gets the full screen JPEG at a specific moment. This should only be called once + during some user-specific setup (for example: when a new user connects) + + get Thumbnail(): Buffer - gets JPEG thumbnail. + + KeyEvent(key: number, pressed: boolean) - sends a key event to the server. + MouseEvent(x: number, y: number, buttons: MouseButtonMask) - sends a mouse event (the button mask is semi-standardized for remoting, + the mask can be converted if not applicable for a given protocol) + + // explicit property setter APIs, maybe expose the semi-internal remotingSetProperty API if required? + set JpegQuality(q: number) - sets JPEG quality + + // events: + + on('open', cb: () => void) - on open + + //on('firstupdate', cb: (rect: RectWithJpeg) => void) - the first update of a resize is given here + // doesn't really matter + + on('resize', cb: (size: Size) => void) - when the server resizes we do too. + + on('update', cb: (rects: Array) => void) - gives screen frame update as jpeg rects + (pre-batched using existing batcher or a new invention or something) + on('close', cb: () => void) - on close + + on('cursor', cb: (b: CursorBitmap) => void) - cursor bitmap changed (always rgba8888) + + } + + binding side API: + + remotingNew("vnc://abc.def:1234") - creates a new remoting client which will use the given protocol in the URI + xxx for callbacks (they will get migrated to eventemitter or something on the JS side so it's more "idiomatic", depending on performance. + In all honesty however, remoting will take care of all the performance sensitive tasks, so it probably won't matter at all) + + remotingConnect(client) -> promise (throws rejection) - disconnects + remotingDisconnect(client) - disconnects + remotingGetBuffer(client) -> Buffer - gets the buffer used for the screen + + remotingSetProperty(client, propertyId, propertyValue) - sets property (e.g: jpeg quality) + e.g: server uri could be set after client creation + with remotingSetProperty(boxedClient, remoting.propertyServerUri, "vnc://another-server.org::2920") + + remotingGetThumbnail(client) - gets thumbnail, this is updated by remoting at about 5 fps + + remotingKeyEvent(client, key, pressed) - key event + remotingMouseEvent(client, x, y, buttons) - mouse event + + on the rust side a boxed client will contain an inner boxed `dyn RemotingProtocolClient` which will contain protocol specific dispatch, + upon parsing a remoting URI we will create a given client (e.g: for `vnc://` we'd make the VNC one) +*/ diff --git a/cvm-rs/index.js b/cvm-rs/index.js new file mode 100644 index 0000000..fc02f40 --- /dev/null +++ b/cvm-rs/index.js @@ -0,0 +1,6 @@ +// *sigh* +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +export let {guacDecode, guacEncode, jpegEncode} = require('./index.node'); + diff --git a/cvm-rs/package.json b/cvm-rs/package.json new file mode 100644 index 0000000..41d9dcb --- /dev/null +++ b/cvm-rs/package.json @@ -0,0 +1,16 @@ +{ + "name": "@cvmts/cvm-rs", + "version": "0.1.0", + "packageManager": "yarn@4.1.1", + "type": "module", + "main": "index.js", + "types": "index.d.ts", + "scripts": { + "build": "cargo-cp-artifact -nc index.node -- cargo build --release --message-format=json-render-diagnostics", + "install": "yarn build", + "test": "cargo test" + }, + "devDependencies": { + "cargo-cp-artifact": "^0.1" + } +} diff --git a/cvm-rs/src/guac.rs b/cvm-rs/src/guac.rs new file mode 100644 index 0000000..b92f289 --- /dev/null +++ b/cvm-rs/src/guac.rs @@ -0,0 +1,193 @@ +use std::fmt; + +// type of a guac message +pub type Elements = Vec; + +// FIXME: thiserror, please. + +/// Errors during decoding +#[derive(Debug, Clone)] +pub enum DecodeError { + /// Invalid guacamole instruction format + InvalidFormat, + + /// Instruction is too long for the current decode policy. + InstructionTooLong, + + /// Element is too long for the current decode policy. + ElementTooLong, + + /// Invalid element size. + ElementSizeInvalid, +} + +pub type DecodeResult = std::result::Result; + +impl fmt::Display for DecodeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidFormat => write!(f, "Invalid Guacamole instruction while decoding"), + Self::InstructionTooLong => write!(f, "Instruction too long for current decode policy"), + Self::ElementTooLong => write!(f, "Element too long for current decode policy"), + Self::ElementSizeInvalid => write!(f, "Element size is invalid") + } + } +} + +// this decode policy abstraction would in theory be useful, +// but idk how to do this kind of thing in rust very well + +pub struct StaticDecodePolicy(); + +impl StaticDecodePolicy { + fn max_instruction_size(&self) -> usize { + INST_SIZE + } + + fn max_element_size(&self) -> usize { + ELEM_SIZE + } +} + +/// The default decode policy. +pub type DefaultDecodePolicy = StaticDecodePolicy<12288, 4096>; + + +/// Encodes elements into a Guacamole instruction +pub fn encode_instruction(elements: &Elements) -> String { + let mut str = String::new(); + + for elem in elements.iter() { + str.push_str(&format!("{}.{},", elem.len(), elem)); + } + + // hacky, but whatever + str.pop(); + str.push(';'); + + str +} + +/// Decodes a Guacamole instruction to individual elements +pub fn decode_instruction(element_string: &String) -> DecodeResult { + let policy = DefaultDecodePolicy {}; + + let mut vec: Elements = Vec::new(); + let mut current_position: usize = 0; + + // Instruction is too long. Don't even bother + if policy.max_instruction_size() < element_string.len() { + return Err(DecodeError::InstructionTooLong); + } + + let chars = element_string.chars().collect::>(); + + loop { + let mut element_size: usize = 0; + + // Scan the integer value in by hand. This is mostly because + // I'm stupid, and the Rust integer parsing routines (seemingly) + // require a substring (or a slice, but, if you can generate a slice, + // you can also just scan the value in by hand.) + // + // We bound this anyways and do quite the checks, so even though it's not great, + // it should be generally fine (TM). + loop { + let c = chars[current_position]; + + if c >= '0' && c <= '9' { + element_size = element_size * 10 + (c as usize) - ('0' as usize); + } else { + if c == '.' { + break; + } + + return Err(DecodeError::InvalidFormat); + } + current_position += 1; + } + + // Eat the '.' seperating the size and the element data; + // our integer scanning ensures we only get here in the case that this is actually the '.' + // character. + current_position += 1; + + // Make sure the element size doesn't overflow the decode policy + // or the size of the whole instruction. + + if element_size >= policy.max_element_size() { + return Err(DecodeError::ElementTooLong); + } + + if element_size >= element_string.len() { + return Err(DecodeError::ElementSizeInvalid); + } + + // cutoff elements or something + if current_position + element_size > chars.len()-1 { + //println!("? {current_position} a {}", chars.len()); + return Err(DecodeError::InvalidFormat); + } + + let element = chars + .iter() + .skip(current_position) + .take(element_size) + .collect::(); + + current_position += element_size; + + vec.push(element); + + // make sure seperator is proper + match chars[current_position] { + ',' => {} + ';' => break, + _ => return Err(DecodeError::InvalidFormat), + } + + // eat the ',' + current_position += 1; + } + + Ok(vec) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decode_basic() { + let test = String::from("7.connect,3.vm1;"); + let res = decode_instruction(&test); + + assert!(res.is_ok()); + assert_eq!(res.unwrap(), vec!["connect", "vm1"]); + } + + #[test] + fn decode_errors() { + let test = String::from("700.connect,3.vm1;"); + let res = decode_instruction(&test); + + eprintln!("Error for: {}", res.clone().unwrap_err()); + + assert!(res.is_err()) + } + + // generally just test that the codec even works + // (we can decode a instruction we created) + #[test] + fn general_codec_works() { + let vec = vec![String::from("connect"), String::from("vm1")]; + let test = encode_instruction(&vec); + + assert_eq!(test, "7.connect,3.vm1;"); + + let res = decode_instruction(&test); + + assert!(res.is_ok()); + assert_eq!(res.unwrap(), vec); + } +} diff --git a/cvm-rs/src/guac_js.rs b/cvm-rs/src/guac_js.rs new file mode 100644 index 0000000..1a8dd01 --- /dev/null +++ b/cvm-rs/src/guac_js.rs @@ -0,0 +1,47 @@ +use neon::prelude::*; +use crate::guac; + +fn guac_decode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsArray> { + let input = cx.argument::(0)?.value(cx); + + match guac::decode_instruction(&input) { + Ok(data) => { + let array = JsArray::new(cx, data.len()); + + let conv = data + .iter() + .map(|v| cx.string(v)) + .collect::>>(); + + for (i, str) in conv.iter().enumerate() { + array.set(cx, i as u32, *str)?; + } + + return Ok(array); + } + + Err(e) => { + return cx.throw_error(format!("{}", e)); + } + } +} + +fn guac_encode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsString> { + let mut elements: Vec = Vec::with_capacity(cx.len()); + + // Capture varadic arguments + for i in 0..cx.len() { + let input = cx.argument::(i)?.value(cx); + elements.push(input); + } + + Ok(cx.string(guac::encode_instruction(&elements))) +} + +pub fn guac_decode(mut cx: FunctionContext) -> JsResult { + guac_decode_impl(&mut cx) +} + +pub fn guac_encode(mut cx: FunctionContext) -> JsResult { + guac_encode_impl(&mut cx) +} diff --git a/cvm-rs/src/jpeg_compressor.rs b/cvm-rs/src/jpeg_compressor.rs new file mode 100644 index 0000000..56248c4 --- /dev/null +++ b/cvm-rs/src/jpeg_compressor.rs @@ -0,0 +1,82 @@ +use turbojpeg_sys::*; + +pub struct Image<'a> { + pub buffer: &'a [u8], + pub width: u32, + pub height: u32, + pub stride: u32, + pub format: TJPF, +} + +pub struct JpegCompressor { + handle: tjhandle, + subsamp: TJSAMP, + quality: u32, +} + +unsafe impl Send for JpegCompressor {} + +impl JpegCompressor { + pub fn new() -> Self { + unsafe { + let init = Self { + handle: tjInitCompress(), + subsamp: TJSAMP_TJSAMP_422, + quality: 95, + }; + return init; + } + } + + pub fn set_quality(&mut self, quality: u32) { + self.quality = quality; + } + + pub fn set_subsamp(&mut self, samp: TJSAMP) { + self.subsamp = samp; + } + + pub fn compress_buffer<'a>(&self, image: &Image<'a>) -> Vec { + unsafe { + let size: usize = + tjBufSize(image.width as i32, image.height as i32, self.subsamp) as usize; + let mut vec = Vec::with_capacity(size); + + vec.resize(size, 0); + + let mut ptr: *mut u8 = vec.as_mut_ptr(); + let mut size: u64 = 0; + + let res = tjCompress2( + self.handle, + image.buffer.as_ptr(), + image.width as i32, + image.stride as i32, + image.height as i32, + image.format, + std::ptr::addr_of_mut!(ptr), + std::ptr::addr_of_mut!(size), + self.subsamp, + self.quality as i32, + (TJFLAG_NOREALLOC) as i32, + ); + + // TODO: Result sex so we can actually notify failure + if res == -1 { + return Vec::new(); + } + + // Truncate down to the size we're given back + vec.truncate(size as usize); + return vec; + } + } +} + +impl Drop for JpegCompressor { + fn drop(&mut self) { + unsafe { + tjDestroy(self.handle); + } + } +} diff --git a/cvm-rs/src/jpeg_js.rs b/cvm-rs/src/jpeg_js.rs new file mode 100644 index 0000000..de87f9b --- /dev/null +++ b/cvm-rs/src/jpeg_js.rs @@ -0,0 +1,87 @@ +use std::sync::{Arc, Mutex}; + +use neon::prelude::*; +use neon::types::buffer::TypedArray; + +use once_cell::sync::OnceCell; +use tokio::runtime::Runtime; + +use std::cell::RefCell; + +use crate::jpeg_compressor::*; + +/// Gives a static Tokio runtime. We should replace this with +/// rayon or something, but for now tokio works. +fn runtime<'a, C: Context<'a>>(cx: &mut C) -> NeonResult<&'static Runtime> { + static RUNTIME: OnceCell = OnceCell::new(); + + RUNTIME + .get_or_try_init(Runtime::new) + .or_else(|err| cx.throw_error(&err.to_string())) +} + +thread_local! { + static COMPRESSOR: RefCell = RefCell::new(JpegCompressor::new()); +} + +fn jpeg_encode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsPromise> { + let input = cx.argument::(0)?; + + // Get our input arguments here + let width: u64 = input.get::(cx, "width")?.value(cx) as u64; + let height: u64 = input.get::(cx, "height")?.value(cx) as u64; + let stride: u64 = input.get::(cx, "stride")?.value(cx) as u64; + let buffer: Handle = input.get(cx, "buffer")?; + + let (deferred, promise) = cx.promise(); + let channel = cx.channel(); + let runtime = runtime(cx)?; + + let buf = buffer.as_slice(cx); + + let copy: Arc>> = Arc::new(Mutex::new(Vec::with_capacity(buf.len()))); + + // Copy from the node buffer to our temporary buffer + { + let mut locked = copy.lock().unwrap(); + let cap = locked.capacity(); + locked.resize(cap, 0); + locked.copy_from_slice(buf); + } + + // Spawn off a tokio blocking pool thread that will do the work for us + runtime.spawn_blocking(move || { + let clone = Arc::clone(©); + let locked = clone.lock().unwrap(); + + let image: Image = Image { + buffer: locked.as_slice(), + width: width as u32, + height: height as u32, + + stride: (stride * 4u64) as u32, // I think? + format: turbojpeg_sys::TJPF_TJPF_RGBA, + }; + + let vec = COMPRESSOR.with(|lazy| { + let mut b = lazy.borrow_mut(); + b.set_quality(35); + b.set_subsamp(turbojpeg_sys::TJSAMP_TJSAMP_420); + b.compress_buffer(&image) + }); + + // Fulfill the Javascript promise with our encoded buffer + deferred.settle_with(&channel, move |mut cx| { + let mut buf = cx.buffer(vec.len())?; + let slice = buf.as_mut_slice(&mut cx); + slice.copy_from_slice(vec.as_slice()); + Ok(buf) + }); + }); + + Ok(promise) +} + +pub fn jpeg_encode(mut cx: FunctionContext) -> JsResult { + jpeg_encode_impl(&mut cx) +} diff --git a/cvm-rs/src/lib.rs b/cvm-rs/src/lib.rs new file mode 100644 index 0000000..e6c6291 --- /dev/null +++ b/cvm-rs/src/lib.rs @@ -0,0 +1,19 @@ +mod guac; +mod guac_js; + +mod jpeg_compressor; +mod jpeg_js; + + +use neon::prelude::*; + + +#[neon::main] +fn main(mut cx: ModuleContext) -> NeonResult<()> { + // Mostly transitionary, later on API should change + cx.export_function("jpegEncode", jpeg_js::jpeg_encode)?; + + cx.export_function("guacDecode", guac_js::guac_decode)?; + cx.export_function("guacEncode", guac_js::guac_encode)?; + Ok(()) +} diff --git a/cvmts/package.json b/cvmts/package.json new file mode 100644 index 0000000..c998bd0 --- /dev/null +++ b/cvmts/package.json @@ -0,0 +1,33 @@ +{ + "name": "@cvmts/cvmts", + "version": "1.0.0", + "description": "replacement for collabvm 1.2.11 because the old one :boom:", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc -outDir dist -rootDir src/", + "serve": "node dist/index.js" + }, + "author": "Elijah R, modeco80", + "license": "GPL-3.0", + "dependencies": { + "@computernewb/nodejs-rfb": "^0.3.0", + "@computernewb/superqemu": "^0.1.0", + "@cvmts/cvm-rs": "*", + "@maxmind/geoip2-node": "^5.0.0", + "execa": "^8.0.1", + "mnemonist": "^0.39.5", + "msgpackr": "^1.10.2", + "pino": "^9.3.1", + "sharp": "^0.33.3", + "toml": "^3.0.0", + "ws": "^8.14.1" + }, + "devDependencies": { + "@types/node": "^20.12.5", + "@types/ws": "^8.5.5", + "pino-pretty": "^11.2.1", + "prettier": "^3.2.5", + "typescript": "^5.4.4" + } +} diff --git a/cvmts/src/AuthManager.ts b/cvmts/src/AuthManager.ts new file mode 100644 index 0000000..4a6ff7f --- /dev/null +++ b/cvmts/src/AuthManager.ts @@ -0,0 +1,42 @@ +import { Rank, User } from './User.js'; + +export default class AuthManager { + apiEndpoint: string; + secretKey: string; + + constructor(apiEndpoint: string, secretKey: string) { + this.apiEndpoint = apiEndpoint; + this.secretKey = secretKey; + } + + async Authenticate(token: string, user: User): Promise { + let response = await fetch(this.apiEndpoint + '/api/v1/join', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + secretKey: this.secretKey, + sessionToken: token, + ip: user.IP.address + }) + }); + + // Make sure the fetch returned okay + if (!response.ok) throw new Error(`Failed to query auth server: ${response.statusText}`); + + let json = (await response.json()) as JoinResponse; + + if (!json.success) throw new Error(json.error); + + return json; + } +} + +interface JoinResponse { + success: boolean; + clientSuccess: boolean; + error: string | undefined; + username: string | undefined; + rank: Rank; +} diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts new file mode 100644 index 0000000..b61c7af --- /dev/null +++ b/cvmts/src/CollabVMServer.ts @@ -0,0 +1,963 @@ +import IConfig from './IConfig.js'; +import * as Utilities from './Utilities.js'; +import { User, Rank } from './User.js'; +import * as cvm from '@cvmts/cvm-rs'; +// I hate that you have to do it like this +import CircularBuffer from 'mnemonist/circular-buffer.js'; +import Queue from 'mnemonist/queue.js'; +import { createHash } from 'crypto'; +import { VMState, QemuVM, QemuVmDefinition } from '@computernewb/superqemu'; +import { IPDataManager } from './IPData.js'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import AuthManager from './AuthManager.js'; +import { JPEGEncoder } from './JPEGEncoder.js'; +import VM from './VM.js'; +import { ReaderModel } from '@maxmind/geoip2-node'; +import * as msgpack from 'msgpackr'; +import { CollabVMProtocolMessage, CollabVMProtocolMessageType } from '@cvmts/collab-vm-1.2-binary-protocol'; + +import { Size, Rect } from './VMDisplay.js'; +import pino from 'pino'; + +// Instead of strange hacks we can just use nodejs provided +// import.meta properties, which have existed since LTS if not before +const __dirname = import.meta.dirname; + +const kCVMTSAssetsRoot = path.resolve(__dirname, '../../assets'); + +const kRestartTimeout = 5000; + +type ChatHistory = { + user: string; + msg: string; +}; + +type VoteTally = { + yes: number; + no: number; +}; + + +export default class CollabVMServer { + private Config: IConfig; + + private clients: User[]; + + private ChatHistory: CircularBuffer; + + private TurnQueue: Queue; + + // Time remaining on the current turn + private TurnTime: number; + + // Interval to keep track of the current turn time + private TurnInterval?: NodeJS.Timeout; + + // If a reset vote is in progress + private voteInProgress: boolean; + + // Interval to keep track of vote resets + private voteInterval?: NodeJS.Timeout; + + // How much time is left on the vote + private voteTime: number; + + // How much time until another reset vote can be cast + private voteCooldown: number; + + // Interval to keep track + private voteCooldownInterval?: NodeJS.Timeout; + + // Completely disable turns + private turnsAllowed: boolean; + + // Hide the screen + private screenHidden: boolean; + + // base64 image to show when the screen is hidden + private screenHiddenImg: string; + private screenHiddenThumb: string; + + // Indefinite turn + private indefiniteTurn: User | null; + private ModPerms: number; + private VM: VM; + + // Authentication manager + private auth: AuthManager | null; + + // Geoip + private geoipReader: ReaderModel | null; + + private logger = pino({ name: 'CVMTS.Server' }); + + constructor(config: IConfig, vm: VM, auth: AuthManager | null, geoipReader: ReaderModel | null) { + this.Config = config; + this.ChatHistory = new CircularBuffer(Array, this.Config.collabvm.maxChatHistoryLength); + this.TurnQueue = new Queue(); + this.TurnTime = 0; + this.clients = []; + this.voteInProgress = false; + this.voteTime = 0; + this.voteCooldown = 0; + this.turnsAllowed = true; + this.screenHidden = false; + this.screenHiddenImg = readFileSync(path.join(kCVMTSAssetsRoot, 'screenhidden.jpeg')).toString('base64'); + this.screenHiddenThumb = readFileSync(path.join(kCVMTSAssetsRoot, 'screenhiddenthumb.jpeg')).toString('base64'); + + this.indefiniteTurn = null; + this.ModPerms = Utilities.MakeModPerms(this.Config.collabvm.moderatorPermissions); + + // No size initially, since there usually won't be a display connected at all during initalization + this.OnDisplayResized({ + width: 0, + height: 0 + }); + + this.VM = vm; + + // this probably should be made general at some point, + // and the VM interface allowed to return a nullable display + // but i cba + let self = this; + if (config.vm.type == 'qemu') { + (vm as QemuVM).on('statechange', (newState: VMState) => { + if (newState == VMState.Started) { + self.logger.info('VM started'); + // well aware this sucks but whatever + self.VM.GetDisplay().on('resize', (size: Size) => self.OnDisplayResized(size)); + self.VM.GetDisplay().on('rect', (rect: Rect) => self.OnDisplayRectangle(rect)); + } + + if (newState == VMState.Stopped) { + setTimeout(async () => { + self.logger.info('restarting VM'); + await self.VM.Start(); + }, kRestartTimeout); + } + }); + } + + // authentication manager + this.auth = auth; + + this.geoipReader = geoipReader; + } + + public addUser(user: User) { + let sameip = this.clients.filter((c) => c.IP.address === user.IP.address); + if (sameip.length >= this.Config.collabvm.maxConnections) { + // Kick the oldest client + // I think this is a better solution than just rejecting the connection + sameip[0].kick(); + } + this.clients.push(user); + if (this.Config.geoip.enabled) { + try { + user.countryCode = this.geoipReader!.country(user.IP.address).country!.isoCode; + } catch (error) { + this.logger.warn(`Failed to get country code for ${user.IP.address}: ${(error as Error).message}`); + } + } + user.socket.on('msg', (msg: string) => this.onMessage(user, msg)); + user.socket.on('disconnect', () => this.connectionClosed(user)); + if (this.Config.auth.enabled) { + user.sendMsg(cvm.guacEncode('auth', this.Config.auth.apiEndpoint)); + } + user.sendMsg(this.getAdduserMsg()); + if (this.Config.geoip.enabled) user.sendMsg(this.getFlagMsg()); + } + + private connectionClosed(user: User) { + let clientIndex = this.clients.indexOf(user); + if (clientIndex === -1) return; + + if (user.IP.vote != null) { + user.IP.vote = null; + this.sendVoteUpdate(); + } + + if (this.indefiniteTurn === user) this.indefiniteTurn = null; + + this.clients.splice(clientIndex, 1); + + this.logger.info(`Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ''}`); + if (!user.username) return; + if (this.TurnQueue.toArray().indexOf(user) !== -1) { + var hadturn = this.TurnQueue.peek() === user; + this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter((u) => u !== user)); + if (hadturn) this.nextTurn(); + } + + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('remuser', '1', user.username!))); + } + + private async onMessage(client: User, message: string) { + try { + var msgArr = cvm.guacDecode(message); + if (msgArr.length < 1) return; + switch (msgArr[0]) { + case 'login': + if (msgArr.length !== 2 || !this.Config.auth.enabled) return; + if (!client.connectedToNode) { + client.sendMsg(cvm.guacEncode('login', '0', 'You must connect to the VM before logging in.')); + return; + } + try { + let res = await this.auth!.Authenticate(msgArr[1], client); + if (res.clientSuccess) { + this.logger.info(`${client.IP.address} logged in as ${res.username}`); + client.sendMsg(cvm.guacEncode('login', '1')); + let old = this.clients.find((c) => c.username === res.username); + if (old) { + // kick() doesnt wait until the user is actually removed from the list and itd be anal to make it do that + // so we call connectionClosed manually here. When it gets called on kick(), it will return because the user isn't in the list + this.connectionClosed(old); + await old.kick(); + } + // Set username + if (client.countryCode !== null && client.noFlag) { + // privacy + for (let cl of this.clients.filter((c) => c !== client)) { + cl.sendMsg(cvm.guacEncode('remuser', '1', client.username!)); + } + this.renameUser(client, res.username, false); + } else this.renameUser(client, res.username, true); + // Set rank + client.rank = res.rank; + if (client.rank === Rank.Admin) { + client.sendMsg(cvm.guacEncode('admin', '0', '1')); + } else if (client.rank === Rank.Moderator) { + client.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString())); + } + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString()))); + } else { + client.sendMsg(cvm.guacEncode('login', '0', res.error!)); + if (res.error === 'You are banned') { + client.kick(); + } + } + } catch (err) { + this.logger.error(`Error authenticating client ${client.IP.address}: ${(err as Error).message}`); + // for now? + client.sendMsg(cvm.guacEncode('login', '0', 'There was an internal error while authenticating. Please let a staff member know as soon as possible')); + } + break; + case 'noflag': { + if (client.connectedToNode) + // too late + return; + client.noFlag = true; + } + case 'list': + if (this.VM.GetState() == VMState.Started) { + client.sendMsg(cvm.guacEncode('list', this.Config.collabvm.node, this.Config.collabvm.displayname, this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail())); + } + break; + case 'connect': + if (!client.username || msgArr.length !== 2 || msgArr[1] !== this.Config.collabvm.node) { + client.sendMsg(cvm.guacEncode('connect', '0')); + return; + } + + client.connectedToNode = true; + client.sendMsg(cvm.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0')); + if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg()); + if (this.Config.collabvm.motd) client.sendMsg(cvm.guacEncode('chat', '', this.Config.collabvm.motd)); + if (this.screenHidden) { + client.sendMsg(cvm.guacEncode('size', '0', '1024', '768')); + client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg)); + } else { + await this.SendFullScreenWithSize(client); + } + client.sendMsg(cvm.guacEncode('sync', Date.now().toString())); + if (this.voteInProgress) this.sendVoteUpdate(client); + this.sendTurnUpdate(client); + break; + case 'view': + if (client.connectedToNode) return; + if (client.username || msgArr.length !== 3 || msgArr[1] !== this.Config.collabvm.node) { + // The use of connect here is intentional. + client.sendMsg(cvm.guacEncode('connect', '0')); + return; + } + + switch (msgArr[2]) { + case '0': + client.viewMode = 0; + break; + case '1': + client.viewMode = 1; + break; + default: + client.sendMsg(cvm.guacEncode('connect', '0')); + return; + } + + client.sendMsg(cvm.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0')); + if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg()); + if (this.Config.collabvm.motd) client.sendMsg(cvm.guacEncode('chat', '', this.Config.collabvm.motd)); + + if (client.viewMode == 1) { + if (this.screenHidden) { + client.sendMsg(cvm.guacEncode('size', '0', '1024', '768')); + client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg)); + } else { + await this.SendFullScreenWithSize(client); + } + client.sendMsg(cvm.guacEncode('sync', Date.now().toString())); + } + + if (this.voteInProgress) this.sendVoteUpdate(client); + this.sendTurnUpdate(client); + break; + case 'rename': + if (!client.RenameRateLimit.request()) return; + if (client.connectedToNode && client.IP.muted) return; + if (this.Config.auth.enabled && client.rank !== Rank.Unregistered) { + client.sendMsg(cvm.guacEncode('chat', '', 'Go to your account settings to change your username.')); + return; + } + if (this.Config.auth.enabled && msgArr[1] !== undefined) { + // Don't send system message to a user without a username since it was likely an automated attempt by the webapp + if (client.username) client.sendMsg(cvm.guacEncode('chat', '', 'You need to log in to do that.')); + if (client.rank !== Rank.Unregistered) return; + this.renameUser(client, undefined); + return; + } + this.renameUser(client, msgArr[1]); + break; + case 'chat': + if (!client.username) return; + if (client.IP.muted) return; + if (msgArr.length !== 2) return; + if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.chat) { + client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.')); + return; + } + var msg = Utilities.HTMLSanitize(msgArr[1]); + // One of the things I hated most about the old server is it completely discarded your message if it was too long + if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength); + if (msg.trim().length < 1) return; + + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msg))); + this.ChatHistory.push({ user: client.username, msg: msg }); + client.onMsgSent(); + break; + case 'turn': + if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && client.rank !== Rank.Admin && client.rank !== Rank.Moderator && client.rank !== Rank.Turn) return; + if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.turn) { + client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.')); + return; + } + if (!client.TurnRateLimit.request()) return; + if (!client.connectedToNode) return; + if (msgArr.length > 2) return; + var takingTurn: boolean; + if (msgArr.length === 1) takingTurn = true; + else + switch (msgArr[1]) { + case '0': + if (this.indefiniteTurn === client) { + this.indefiniteTurn = null; + } + takingTurn = false; + break; + case '1': + takingTurn = true; + break; + default: + return; + break; + } + if (takingTurn) { + var currentQueue = this.TurnQueue.toArray(); + // If the user is already in the turn queue, ignore the turn request. + if (currentQueue.indexOf(client) !== -1) return; + // If they're muted, also ignore the turn request. + // Send them the turn queue to prevent client glitches + if (client.IP.muted) return; + if (this.Config.collabvm.turnlimit.enabled) { + // Get the amount of users in the turn queue with the same IP as the user requesting a turn. + let turns = currentQueue.filter((user) => user.IP.address == client.IP.address); + // If it exceeds the limit set in the config, ignore the turn request. + if (turns.length + 1 > this.Config.collabvm.turnlimit.maximum) return; + } + this.TurnQueue.enqueue(client); + if (this.TurnQueue.size === 1) this.nextTurn(); + } else { + var hadturn = this.TurnQueue.peek() === client; + this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter((u) => u !== client)); + if (hadturn) this.nextTurn(); + } + this.sendTurnUpdate(); + break; + case 'mouse': + if (this.TurnQueue.peek() !== client && client.rank !== Rank.Admin) return; + var x = parseInt(msgArr[1]); + var y = parseInt(msgArr[2]); + var mask = parseInt(msgArr[3]); + if (x === undefined || y === undefined || mask === undefined) return; + this.VM.GetDisplay()?.MouseEvent(x, y, mask); + break; + case 'key': + if (this.TurnQueue.peek() !== client && client.rank !== Rank.Admin) return; + var keysym = parseInt(msgArr[1]); + var down = parseInt(msgArr[2]); + if (keysym === undefined || (down !== 0 && down !== 1)) return; + this.VM.GetDisplay()?.KeyboardEvent(keysym, down === 1 ? true : false); + break; + case 'vote': + if (!this.VM.SnapshotsSupported()) return; + if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && client.rank !== Rank.Admin && client.rank !== Rank.Moderator && client.rank !== Rank.Turn) return; + if (!client.connectedToNode) return; + if (msgArr.length !== 2) return; + if (!client.VoteRateLimit.request()) return; + switch (msgArr[1]) { + case '1': + if (!this.voteInProgress) { + if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.callForReset) { + client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.')); + return; + } + + if (this.voteCooldown !== 0) { + client.sendMsg(cvm.guacEncode('vote', '3', this.voteCooldown.toString())); + return; + } + this.startVote(); + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', `${client.username} has started a vote to reset the VM.`))); + } + if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.vote) { + client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.')); + return; + } else if (client.IP.vote !== true) { + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', `${client.username} has voted yes.`))); + } + client.IP.vote = true; + break; + case '0': + if (!this.voteInProgress) return; + if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.vote) { + client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.')); + return; + } + if (client.IP.vote !== false) { + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', `${client.username} has voted no.`))); + } + client.IP.vote = false; + break; + } + this.sendVoteUpdate(); + break; + case 'cap': { + if (msgArr.length < 2) return; + // Capabilities can only be announced before connecting to the VM + if (client.connectedToNode) return; + var caps = []; + for (const cap of msgArr.slice(1)) + switch (cap) { + case 'bin': { + if (caps.indexOf('bin') !== -1) break; + client.Capabilities.bin = true; + caps.push('bin'); + break; + } + } + client.sendMsg(cvm.guacEncode('cap', ...caps)); + } + case 'admin': + if (msgArr.length < 2) return; + switch (msgArr[1]) { + case '2': + // Login + if (this.Config.auth.enabled) { + client.sendMsg(cvm.guacEncode('chat', '', 'This server does not support staff passwords. Please log in to become staff.')); + return; + } + if (!client.LoginRateLimit.request() || !client.username) return; + if (msgArr.length !== 3) return; + var sha256 = createHash('sha256'); + sha256.update(msgArr[2]); + var pwdHash = sha256.digest('hex'); + sha256.destroy(); + if (pwdHash === this.Config.collabvm.adminpass) { + client.rank = Rank.Admin; + client.sendMsg(cvm.guacEncode('admin', '0', '1')); + } else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) { + client.rank = Rank.Moderator; + client.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString())); + } else if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) { + client.rank = Rank.Turn; + client.sendMsg(cvm.guacEncode('chat', '', 'You may now take turns.')); + } else { + client.sendMsg(cvm.guacEncode('admin', '0', '0')); + return; + } + if (this.screenHidden) { + await this.SendFullScreenWithSize(client); + + client.sendMsg(cvm.guacEncode('sync', Date.now().toString())); + } + + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString()))); + break; + case '5': + // QEMU Monitor + if (client.rank !== Rank.Admin) return; + if (msgArr.length !== 4 || msgArr[2] !== this.Config.collabvm.node) return; + let output = await this.VM.MonitorCommand(msgArr[3]); + client.sendMsg(cvm.guacEncode('admin', '2', String(output))); + break; + case '8': + // Restore + if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.restore)) return; + this.VM.Reset(); + break; + case '10': + // Reboot + if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.reboot)) return; + if (msgArr.length !== 3 || msgArr[2] !== this.Config.collabvm.node) return; + await this.VM.Reboot(); + break; + case '12': + // Ban + if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.ban)) return; + var user = this.clients.find((c) => c.username === msgArr[2]); + if (!user) return; + user.ban(); + case '13': + // Force Vote + if (msgArr.length !== 3) return; + if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.forcevote)) return; + if (!this.voteInProgress) return; + switch (msgArr[2]) { + case '1': + this.endVote(true); + break; + case '0': + this.endVote(false); + break; + } + break; + case '14': + // Mute + if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.mute)) return; + if (msgArr.length !== 4) return; + var user = this.clients.find((c) => c.username === msgArr[2]); + if (!user) return; + var permamute; + switch (msgArr[3]) { + case '0': + permamute = false; + break; + case '1': + permamute = true; + break; + default: + return; + } + user.mute(permamute); + break; + case '15': + // Kick + if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.kick)) return; + var user = this.clients.find((c) => c.username === msgArr[2]); + if (!user) return; + user.kick(); + break; + case '16': + // End turn + if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return; + if (msgArr.length !== 3) return; + var user = this.clients.find((c) => c.username === msgArr[2]); + if (!user) return; + this.endTurn(user); + break; + case '17': + // Clear turn queue + if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return; + if (msgArr.length !== 3 || msgArr[2] !== this.Config.collabvm.node) return; + this.clearTurns(); + break; + case '18': + // Rename user + if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return; + if (this.Config.auth.enabled) { + client.sendMsg(cvm.guacEncode('chat', '', 'Cannot rename users on a server that uses authentication.')); + } + if (msgArr.length !== 4) return; + var user = this.clients.find((c) => c.username === msgArr[2]); + if (!user) return; + this.renameUser(user, msgArr[3]); + break; + case '19': + // Get IP + if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.grabip)) return; + if (msgArr.length !== 3) return; + var user = this.clients.find((c) => c.username === msgArr[2]); + if (!user) return; + client.sendMsg(cvm.guacEncode('admin', '19', msgArr[2], user.IP.address)); + break; + case '20': + // Steal turn + if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return; + this.bypassTurn(client); + break; + case '21': + // XSS + if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.xss)) return; + if (msgArr.length !== 3) return; + switch (client.rank) { + case Rank.Admin: + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msgArr[2]))); + + this.ChatHistory.push({ user: client.username!, msg: msgArr[2] }); + break; + case Rank.Moderator: + this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msgArr[2]))); + + this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, Utilities.HTMLSanitize(msgArr[2])))); + break; + } + break; + case '22': + // Toggle turns + if (client.rank !== Rank.Admin) return; + if (msgArr.length !== 3) return; + switch (msgArr[2]) { + case '0': + this.clearTurns(); + this.turnsAllowed = false; + break; + case '1': + this.turnsAllowed = true; + break; + } + break; + case '23': + // Indefinite turn + if (client.rank !== Rank.Admin) return; + this.indefiniteTurn = client; + this.TurnQueue = Queue.from([client, ...this.TurnQueue.toArray().filter((c) => c !== client)]); + this.sendTurnUpdate(); + break; + case '24': + // Hide screen + if (client.rank !== Rank.Admin) return; + if (msgArr.length !== 3) return; + switch (msgArr[2]) { + case '0': + this.screenHidden = true; + this.clients + .filter((c) => c.rank == Rank.Unregistered) + .forEach((client) => { + client.sendMsg(cvm.guacEncode('size', '0', '1024', '768')); + client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg)); + client.sendMsg(cvm.guacEncode('sync', Date.now().toString())); + }); + break; + case '1': + this.screenHidden = false; + let displaySize = this.VM.GetDisplay().Size(); + + let encoded = await this.MakeRectData({ + x: 0, + y: 0, + width: displaySize.width, + height: displaySize.height + }); + + this.clients.forEach(async (client) => this.SendFullScreenWithSize(client)); + break; + } + break; + case '25': + if (client.rank !== Rank.Admin || msgArr.length !== 3) return; + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', msgArr[2]))); + break; + } + break; + } + } catch (err) { + // No + this.logger.error(`User ${user?.IP.address} ${user?.username ? `with username ${user?.username}` : ''} sent broken Guacamole: ${err as Error}`); + user?.kick(); + } + } + + getUsernameList(): string[] { + var arr: string[] = []; + + this.clients.filter((c) => c.username).forEach((c) => arr.push(c.username!)); + return arr; + } + + renameUser(client: User, newName?: string, announce: boolean = true) { + // This shouldn't need a ternary but it does for some reason + var hadName: boolean = client.username ? true : false; + var oldname: any; + if (hadName) oldname = client.username; + var status = '0'; + if (!newName) { + client.assignGuestName(this.getUsernameList()); + } else { + newName = newName.trim(); + if (hadName && newName === oldname) { + client.sendMsg(cvm.guacEncode('rename', '0', '0', client.username!, client.rank.toString())); + return; + } + if (this.getUsernameList().indexOf(newName) !== -1) { + client.assignGuestName(this.getUsernameList()); + if (client.connectedToNode) { + status = '1'; + } + } else if (!/^[a-zA-Z0-9\ \-\_\.]+$/.test(newName) || newName.length > 20 || newName.length < 3) { + client.assignGuestName(this.getUsernameList()); + status = '2'; + } else if (this.Config.collabvm.usernameblacklist.indexOf(newName) !== -1) { + client.assignGuestName(this.getUsernameList()); + status = '3'; + } else client.username = newName; + } + + client.sendMsg(cvm.guacEncode('rename', '0', status, client.username!, client.rank.toString())); + if (hadName) { + this.logger.info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`); + if (announce) this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('rename', '1', oldname, client.username!, client.rank.toString()))); + } else { + this.logger.info(`Rename ${client.IP.address} to ${client.username}`); + if (announce) + this.clients.forEach((c) => { + c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString())); + if (client.countryCode !== null) c.sendMsg(cvm.guacEncode('flag', client.username!, client.countryCode)); + }); + } + } + + getAdduserMsg(): string { + var arr: string[] = ['adduser', this.clients.filter((c) => c.username).length.toString()]; + + this.clients.filter((c) => c.username).forEach((c) => arr.push(c.username!, c.rank.toString())); + return cvm.guacEncode(...arr); + } + + getFlagMsg(): string { + var arr = ['flag']; + for (let c of this.clients.filter((cl) => cl.countryCode !== null && cl.username && (!cl.noFlag || cl.rank === Rank.Unregistered))) { + arr.push(c.username!, c.countryCode!); + } + return cvm.guacEncode(...arr); + } + + getChatHistoryMsg(): string { + var arr: string[] = ['chat']; + this.ChatHistory.forEach((c) => arr.push(c.user, c.msg)); + return cvm.guacEncode(...arr); + } + + private sendTurnUpdate(client?: User) { + var turnQueueArr = this.TurnQueue.toArray(); + var turntime; + if (this.indefiniteTurn === null) turntime = this.TurnTime * 1000; + else turntime = 9999999999; + var arr = ['turn', turntime.toString(), this.TurnQueue.size.toString()]; + // @ts-ignore + this.TurnQueue.forEach((c) => arr.push(c.username)); + var currentTurningUser = this.TurnQueue.peek(); + if (client) { + client.sendMsg(cvm.guacEncode(...arr)); + return; + } + this.clients + .filter((c) => c !== currentTurningUser && c.connectedToNode) + .forEach((c) => { + if (turnQueueArr.indexOf(c) !== -1) { + var time; + if (this.indefiniteTurn === null) time = this.TurnTime * 1000 + (turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000; + else time = 9999999999; + c.sendMsg(cvm.guacEncode(...arr, time.toString())); + } else { + c.sendMsg(cvm.guacEncode(...arr)); + } + }); + if (currentTurningUser) currentTurningUser.sendMsg(cvm.guacEncode(...arr)); + } + private nextTurn() { + clearInterval(this.TurnInterval); + if (this.TurnQueue.size === 0) { + } else { + this.TurnTime = this.Config.collabvm.turnTime; + this.TurnInterval = setInterval(() => this.turnInterval(), 1000); + } + this.sendTurnUpdate(); + } + + clearTurns() { + clearInterval(this.TurnInterval); + this.TurnQueue.clear(); + this.sendTurnUpdate(); + } + + bypassTurn(client: User) { + var a = this.TurnQueue.toArray().filter((c) => c !== client); + this.TurnQueue = Queue.from([client, ...a]); + this.nextTurn(); + } + + endTurn(client: User) { + var hasTurn = this.TurnQueue.peek() === client; + this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter((c) => c !== client)); + if (hasTurn) this.nextTurn(); + else this.sendTurnUpdate(); + } + + private turnInterval() { + if (this.indefiniteTurn !== null) return; + this.TurnTime--; + if (this.TurnTime < 1) { + this.TurnQueue.dequeue(); + this.nextTurn(); + } + } + + private async OnDisplayRectangle(rect: Rect) { + let encoded = await this.MakeRectData(rect); + let encodedb64 = encoded.toString('base64'); + let bmsg: CollabVMProtocolMessage = { + type: CollabVMProtocolMessageType.rect, + rect: { + x: rect.x, + y: rect.y, + data: encoded + } + }; + var encodedbin = msgpack.encode(bmsg); + this.clients + .filter((c) => c.connectedToNode || c.viewMode == 1) + .forEach((c) => { + if (this.screenHidden && c.rank == Rank.Unregistered) return; + if (c.Capabilities.bin) { + c.socket.sendBinary(encodedbin); + } else { + c.sendMsg(cvm.guacEncode('png', '0', '0', rect.x.toString(), rect.y.toString(), encodedb64)); + c.sendMsg(cvm.guacEncode('sync', Date.now().toString())); + } + }); + } + + private OnDisplayResized(size: Size) { + this.clients + .filter((c) => c.connectedToNode || c.viewMode == 1) + .forEach((c) => { + if (this.screenHidden && c.rank == Rank.Unregistered) return; + c.sendMsg(cvm.guacEncode('size', '0', size.width.toString(), size.height.toString())); + }); + } + + private async SendFullScreenWithSize(client: User) { + let display = this.VM.GetDisplay(); + if (display == null) return; + + let displaySize = display.Size(); + + let encoded = await this.MakeRectData({ + x: 0, + y: 0, + width: displaySize.width, + height: displaySize.height + }); + + client.sendMsg(cvm.guacEncode('size', '0', displaySize.width.toString(), displaySize.height.toString())); + + if (client.Capabilities.bin) { + let msg: CollabVMProtocolMessage = { + type: CollabVMProtocolMessageType.rect, + rect: { + x: 0, + y: 0, + data: encoded + } + }; + client.socket.sendBinary(msgpack.encode(msg)); + } else { + client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', encoded.toString('base64'))); + } + } + + private async MakeRectData(rect: Rect) { + let display = this.VM.GetDisplay(); + + // TODO: actually throw an error here + if (display == null) return Buffer.from('no'); + + let displaySize = display.Size(); + let encoded = await JPEGEncoder.Encode(display.Buffer(), displaySize, rect); + + return encoded; + } + + async getThumbnail(): Promise { + let display = this.VM.GetDisplay(); + + // oh well + if (!display.Connected()) return ''; + + let buf = await JPEGEncoder.EncodeThumbnail(display.Buffer(), display.Size()); + return buf.toString('base64'); + } + + startVote() { + if (this.voteInProgress) return; + this.voteInProgress = true; + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('vote', '0'))); + this.voteTime = this.Config.collabvm.voteTime; + this.voteInterval = setInterval(() => { + this.voteTime--; + if (this.voteTime < 1) { + this.endVote(); + } + }, 1000); + } + + endVote(result?: boolean) { + if (!this.voteInProgress) return; + this.voteInProgress = false; + clearInterval(this.voteInterval); + var count = this.getVoteCounts(); + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('vote', '2'))); + if (result === true || (result === undefined && count.yes >= count.no)) { + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', 'The vote to reset the VM has won.'))); + this.VM.Reset(); + } else { + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', 'The vote to reset the VM has lost.'))); + } + this.clients.forEach((c) => { + c.IP.vote = null; + }); + this.voteCooldown = this.Config.collabvm.voteCooldown; + this.voteCooldownInterval = setInterval(() => { + this.voteCooldown--; + if (this.voteCooldown < 1) clearInterval(this.voteCooldownInterval); + }, 1000); + } + + sendVoteUpdate(client?: User) { + if (!this.voteInProgress) return; + var count = this.getVoteCounts(); + var msg = cvm.guacEncode('vote', '1', (this.voteTime * 1000).toString(), count.yes.toString(), count.no.toString()); + if (client) client.sendMsg(msg); + else this.clients.forEach((c) => c.sendMsg(msg)); + } + + getVoteCounts(): VoteTally { + let yes = 0; + let no = 0; + IPDataManager.ForEachIPData((c) => { + if (c.vote === true) yes++; + if (c.vote === false) no++; + }); + return { yes: yes, no: no }; + } +} diff --git a/cvmts/src/GeoIPDownloader.ts b/cvmts/src/GeoIPDownloader.ts new file mode 100644 index 0000000..e53a067 --- /dev/null +++ b/cvmts/src/GeoIPDownloader.ts @@ -0,0 +1,105 @@ +import { Reader, ReaderModel } from '@maxmind/geoip2-node'; +import * as fs from 'fs/promises'; +import * as path from 'node:path'; +import { Readable } from 'node:stream'; +import { ReadableStream } from 'node:stream/web'; +import { finished } from 'node:stream/promises'; +import { execa } from 'execa'; +import pino from 'pino'; + +export default class GeoIPDownloader { + private directory: string; + private accountID: string; + private licenseKey: string; + private logger = pino({ name: 'CVMTS.GeoIPDownloader' }); + constructor(filename: string, accountID: string, licenseKey: string) { + this.directory = filename; + if (!this.directory.endsWith('/')) this.directory += '/'; + this.accountID = accountID; + this.licenseKey = licenseKey; + } + + private genAuthHeader(): string { + return `Basic ${Buffer.from(`${this.accountID}:${this.licenseKey}`).toString('base64')}`; + } + + private async ensureDirectoryExists(): Promise { + let stat; + try { + stat = await fs.stat(this.directory); + } catch (e) { + var error = e as NodeJS.ErrnoException; + if (error.code === 'ENOTDIR') { + this.logger.warn('File exists at GeoIP directory path, unlinking...'); + await fs.unlink(this.directory.substring(0, this.directory.length - 1)); + } else if (error.code !== 'ENOENT') { + this.logger.error('Failed to access GeoIP directory: {0}', error.message); + process.exit(1); + } + this.logger.info('Creating GeoIP directory: {0}', this.directory); + await fs.mkdir(this.directory, { recursive: true }); + return; + } + } + + async getGeoIPReader(): Promise { + await this.ensureDirectoryExists(); + let dbpath = path.join(this.directory, (await this.getLatestVersion()).replace('.tar.gz', ''), 'GeoLite2-Country.mmdb'); + try { + await fs.access(dbpath, fs.constants.F_OK | fs.constants.R_OK); + this.logger.info('Loading cached GeoIP database: {0}', dbpath); + } catch (ex) { + var error = ex as NodeJS.ErrnoException; + if (error.code === 'ENOENT') { + await this.downloadLatestDatabase(); + } else { + this.logger.error('Failed to access GeoIP database: {0}', error.message); + process.exit(1); + } + } + return await Reader.open(dbpath); + } + + async getLatestVersion(): Promise { + let res = await fetch('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', { + redirect: 'follow', + method: 'HEAD', + headers: { + Authorization: this.genAuthHeader() + } + }); + let disposition = res.headers.get('Content-Disposition'); + if (!disposition) { + this.logger.error('Failed to get latest version of GeoIP database: No Content-Disposition header'); + process.exit(1); + } + let filename = disposition.match(/filename=(.*)$/); + if (!filename) { + this.logger.error('Failed to get latest version of GeoIP database: Could not parse version from Content-Disposition header'); + process.exit(1); + } + return filename[1]; + } + + async downloadLatestDatabase(): Promise { + let filename = await this.getLatestVersion(); + this.logger.info('Downloading latest GeoIP database: {0}', filename); + let dbpath = path.join(this.directory, filename); + let file = await fs.open(dbpath, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY); + let stream = file.createWriteStream(); + let res = await fetch('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', { + redirect: 'follow', + headers: { + Authorization: this.genAuthHeader() + } + }); + await finished(Readable.fromWeb(res.body as ReadableStream).pipe(stream)); + await file.close(); + this.logger.info('Finished downloading latest GeoIP database: {0}', filename); + this.logger.info('Extracting GeoIP database: {0}', filename); + // yeah whatever + await execa('tar', ['xzf', filename], { cwd: this.directory }); + this.logger.info('Unlinking GeoIP tarball'); + await fs.unlink(dbpath); + } +} diff --git a/cvmts/src/IConfig.ts b/cvmts/src/IConfig.ts new file mode 100644 index 0000000..6b72adc --- /dev/null +++ b/cvmts/src/IConfig.ts @@ -0,0 +1,88 @@ +import VNCVMDef from './VNCVM/VNCVMDef'; + +export default interface IConfig { + http: { + host: string; + port: number; + proxying: boolean; + proxyAllowedIps: string[]; + origin: boolean; + originAllowedDomains: string[]; + }; + geoip: { + enabled: boolean; + directory: string; + accountID: string; + licenseKey: string; + } + tcp: { + enabled: boolean; + host: string; + port: number; + }; + auth: { + enabled: boolean; + apiEndpoint: string; + secretKey: string; + guestPermissions: { + chat: boolean; + turn: boolean; + callForReset: boolean; + vote: boolean; + }; + }; + vm: { + type: 'qemu' | 'vncvm'; + }; + qemu: { + qemuArgs: string; + vncPort: number; + snapshots: boolean; + qmpHost: string | null; + qmpPort: number | null; + qmpSockDir: string | null; + }; + vncvm: VNCVMDef; + collabvm: { + node: string; + displayname: string; + motd: string; + maxConnections: number; + bancmd: string | string[]; + moderatorEnabled: boolean; + usernameblacklist: string[]; + maxChatLength: number; + maxChatHistoryLength: number; + turnlimit: { + enabled: boolean; + maximum: number; + }; + automute: { + enabled: boolean; + seconds: number; + messages: number; + }; + tempMuteTime: number; + turnTime: number; + voteTime: number; + voteCooldown: number; + adminpass: string; + modpass: string; + turnwhitelist: boolean; + turnpass: string; + moderatorPermissions: Permissions; + }; +} + +export interface Permissions { + restore: boolean; + reboot: boolean; + ban: boolean; + forcevote: boolean; + mute: boolean; + kick: boolean; + bypassturn: boolean; + rename: boolean; + grabip: boolean; + xss: boolean; +} diff --git a/cvmts/src/IPData.ts b/cvmts/src/IPData.ts new file mode 100644 index 0000000..4228fa9 --- /dev/null +++ b/cvmts/src/IPData.ts @@ -0,0 +1,71 @@ +import pino from 'pino'; + +export class IPData { + tempMuteExpireTimeout?: NodeJS.Timeout; + muted: Boolean; + vote: boolean | null; + address: string; + refCount: number = 0; + + constructor(address: string) { + this.address = address; + this.muted = false; + this.vote = null; + } + + // Call when a connection is closed to "release" the ip data + Unref() { + if (this.refCount - 1 < 0) this.refCount = 0; + else this.refCount--; + } +} + +export class IPDataManager { + static ipDatas = new Map(); + static logger = pino({ name: 'CVMTS.IPDataManager' }); + + static GetIPData(address: string) { + if (IPDataManager.ipDatas.has(address)) { + // Note: We already check for if it exists, so we use ! here + // because TypeScript can't exactly tell that in this case, + // only in explicit null or undefined checks + let ref = IPDataManager.ipDatas.get(address)!; + ref.refCount++; + return ref; + } + + let data = new IPData(address); + data.refCount++; + IPDataManager.ipDatas.set(address, data); + return data; + } + + static GetIPDataMaybe(address: string) { + if (IPDataManager.ipDatas.has(address)) { + // Note: We already check for if it exists, so we use ! here + // because TypeScript can't exactly tell that in this case, + // only in explicit null or undefined checks + let ref = IPDataManager.ipDatas.get(address)!; + ref.refCount++; + return ref; + } + + return null; + } + + static ForEachIPData(callback: (d: IPData) => void) { + for (let tuple of IPDataManager.ipDatas) callback(tuple[1]); + } +} + +// Garbage collect unreferenced IPDatas every 15 seconds. +// Strictly speaking this will just allow the v8 GC to finally +// delete the objects, but same difference. +setInterval(() => { + for (let tuple of IPDataManager.ipDatas) { + if (tuple[1].refCount == 0) { + IPDataManager.logger.info(`Deleted IPData for IP ${tuple[0]}`); + IPDataManager.ipDatas.delete(tuple[0]); + } + } +}, 15000); diff --git a/cvmts/src/JPEGEncoder.ts b/cvmts/src/JPEGEncoder.ts new file mode 100644 index 0000000..763b3c5 --- /dev/null +++ b/cvmts/src/JPEGEncoder.ts @@ -0,0 +1,52 @@ +import { Size, Rect } from './VMDisplay.js'; +import sharp from 'sharp'; +import * as cvm from '@cvmts/cvm-rs'; + +// A good balance. TODO: Configurable? +let gJpegQuality = 35; + +const kThumbnailSize: Size = { + width: 400, + height: 300 +}; + +// this returns appropiate Sharp options to deal with CVMTS raw framebuffers +// (which are RGBA bitmaps, essentially. We probably should abstract that out but +// that'd mean having to introduce that to rfb and oihwekjtgferklds;./tghnredsltg;erhds) +function GetRawSharpOptions(size: Size): sharp.CreateRaw { + return { + width: size.width, + height: size.height, + channels: 4 + }; +} + +export class JPEGEncoder { + static SetQuality(quality: number) { + gJpegQuality = quality; + } + + static async Encode(canvas: Buffer, displaySize: Size, rect: Rect): Promise { + let offset = (rect.y * displaySize.width + rect.x) * 4; + return cvm.jpegEncode({ + width: rect.width, + height: rect.height, + stride: displaySize.width, + buffer: canvas.subarray(offset) + }); + } + + static async EncodeThumbnail(buffer: Buffer, size: Size): Promise { + let { data, info } = await sharp(buffer, { raw: GetRawSharpOptions(size) }) + .resize(kThumbnailSize.width, kThumbnailSize.height, { fit: 'fill' }) + .raw() + .toBuffer({ resolveWithObject: true }); + + return cvm.jpegEncode({ + width: kThumbnailSize.width, + height: kThumbnailSize.height, + stride: kThumbnailSize.width, + buffer: data + }); + } +} diff --git a/cvmts/src/NetworkClient.ts b/cvmts/src/NetworkClient.ts new file mode 100644 index 0000000..600356f --- /dev/null +++ b/cvmts/src/NetworkClient.ts @@ -0,0 +1,9 @@ +export default interface NetworkClient { + getIP(): string; + send(msg: string): Promise; + sendBinary(msg: Uint8Array): Promise; + close(): void; + on(event: string, listener: (...args: any[]) => void): void; + off(event: string, listener: (...args: any[]) => void): void; + isOpen(): boolean; +} diff --git a/cvmts/src/NetworkServer.ts b/cvmts/src/NetworkServer.ts new file mode 100644 index 0000000..5fce6f6 --- /dev/null +++ b/cvmts/src/NetworkServer.ts @@ -0,0 +1,6 @@ +export default interface NetworkServer { + start(): void; + stop(): void; + on(event: string, listener: (...args: any[]) => void): void; + off(event: string, listener: (...args: any[]) => void): void; +} diff --git a/cvmts/src/RateLimiter.ts b/cvmts/src/RateLimiter.ts new file mode 100644 index 0000000..3df21fb --- /dev/null +++ b/cvmts/src/RateLimiter.ts @@ -0,0 +1,36 @@ +import { EventEmitter } from 'events'; + +// Class to ratelimit a resource (chatting, logging in, etc) +export default class RateLimiter extends EventEmitter { + private limit: number; + private interval: number; + private requestCount: number; + private limiter?: NodeJS.Timeout; + private limiterSet: boolean; + constructor(limit: number, interval: number) { + super(); + this.limit = limit; + this.interval = interval; + this.requestCount = 0; + this.limiterSet = false; + } + // Return value is whether or not the action should be continued + request(): boolean { + this.requestCount++; + if (this.requestCount === this.limit) { + this.emit('limit'); + clearTimeout(this.limiter); + this.limiterSet = false; + this.requestCount = 0; + return false; + } + if (!this.limiterSet) { + this.limiter = setTimeout(() => { + this.limiterSet = false; + this.requestCount = 0; + }, this.interval * 1000); + this.limiterSet = true; + } + return true; + } +} diff --git a/cvmts/src/TCP/TCPClient.ts b/cvmts/src/TCP/TCPClient.ts new file mode 100644 index 0000000..f20498e --- /dev/null +++ b/cvmts/src/TCP/TCPClient.ts @@ -0,0 +1,72 @@ +import EventEmitter from 'events'; +import NetworkClient from '../NetworkClient.js'; +import { Socket } from 'net'; + +const TextHeader = 0; +const BinaryHeader = 1; + +export default class TCPClient extends EventEmitter implements NetworkClient { + private socket: Socket; + private cache: string; + + constructor(socket: Socket) { + super(); + this.socket = socket; + this.cache = ''; + this.socket.on('end', () => { + this.emit('disconnect'); + }); + this.socket.on('data', (data) => { + var msg = data.toString('utf-8'); + if (msg[msg.length - 1] === '\n') msg = msg.slice(0, -1); + this.cache += msg; + this.readCache(); + }); + } + + private readCache() { + for (var index = this.cache.indexOf(';'); index !== -1; index = this.cache.indexOf(';')) { + this.emit('msg', this.cache.slice(0, index + 1)); + this.cache = this.cache.slice(index + 1); + } + } + + getIP(): string { + return this.socket.remoteAddress!; + } + + send(msg: string): Promise { + return new Promise((res, rej) => { + let _msg = new Uint32Array([TextHeader, ...Buffer.from(msg, "utf-8")]); + this.socket.write(Buffer.from(_msg), (err) => { + if (err) { + rej(err); + return; + } + res(); + }); + }); + } + + sendBinary(msg: Uint8Array): Promise { + return new Promise((res, rej) => { + let _msg = new Uint32Array([BinaryHeader, msg.length, ...msg]); + this.socket.write(Buffer.from(_msg), (err) => { + if (err) { + rej(err); + return; + } + res(); + }); + }); + } + + close(): void { + this.emit('disconnect'); + this.socket.end(); + } + + isOpen(): boolean { + return this.socket.writable; + } +} diff --git a/cvmts/src/TCP/TCPServer.ts b/cvmts/src/TCP/TCPServer.ts new file mode 100644 index 0000000..040a81a --- /dev/null +++ b/cvmts/src/TCP/TCPServer.ts @@ -0,0 +1,39 @@ +import EventEmitter from 'events'; +import NetworkServer from '../NetworkServer.js'; +import { Server, Socket } from 'net'; +import IConfig from '../IConfig.js'; +import TCPClient from './TCPClient.js'; +import { IPDataManager } from '../IPData.js'; +import { User } from '../User.js'; +import pino from 'pino'; + +export default class TCPServer extends EventEmitter implements NetworkServer { + listener: Server; + Config: IConfig; + logger = pino({name: 'CVMTS.TCPServer'}); + clients: TCPClient[]; + + constructor(config: IConfig) { + super(); + this.Config = config; + this.listener = new Server(); + this.clients = []; + this.listener.on('connection', (socket) => this.onConnection(socket)); + } + + private onConnection(socket: Socket) { + this.logger.info(`New TCP connection from ${socket.remoteAddress}`); + var client = new TCPClient(socket); + this.clients.push(client); + this.emit('connect', new User(client, IPDataManager.GetIPData(client.getIP()), this.Config)); + } + + start(): void { + this.listener.listen(this.Config.tcp.port, this.Config.tcp.host, () => { + this.logger.info(`TCP server listening on ${this.Config.tcp.host}:${this.Config.tcp.port}`); + }); + } + stop(): void { + this.listener.close(); + } +} diff --git a/cvmts/src/User.ts b/cvmts/src/User.ts new file mode 100644 index 0000000..a9cb62b --- /dev/null +++ b/cvmts/src/User.ts @@ -0,0 +1,181 @@ +import * as Utilities from './Utilities.js'; +import * as cvm from '@cvmts/cvm-rs'; +import { IPData } from './IPData.js'; +import IConfig from './IConfig.js'; +import RateLimiter from './RateLimiter.js'; +import { execa, execaCommand, ExecaSyncError } from 'execa'; +import NetworkClient from './NetworkClient.js'; +import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol'; +import pino from 'pino'; + +export class User { + socket: NetworkClient; + nopSendInterval: NodeJS.Timeout; + msgRecieveInterval: NodeJS.Timeout; + nopRecieveTimeout?: NodeJS.Timeout; + username?: string; + connectedToNode: boolean; + viewMode: number; + rank: Rank; + msgsSent: number; + Config: IConfig; + IP: IPData; + Capabilities: CollabVMCapabilities; + // Hide flag. Only takes effect if the user is logged in. + noFlag: boolean = false; + countryCode: string | null = null; + // Rate limiters + ChatRateLimit: RateLimiter; + LoginRateLimit: RateLimiter; + RenameRateLimit: RateLimiter; + TurnRateLimit: RateLimiter; + VoteRateLimit: RateLimiter; + + private logger = pino({ name: 'CVMTS.User' }); + + constructor(socket: NetworkClient, ip: IPData, config: IConfig, username?: string, node?: string) { + this.IP = ip; + this.connectedToNode = false; + this.viewMode = -1; + this.Config = config; + this.socket = socket; + this.msgsSent = 0; + this.Capabilities = new CollabVMCapabilities(); + + this.socket.on('disconnect', () => { + // Unref the ip data for this connection + this.IP.Unref(); + + clearInterval(this.nopSendInterval); + clearInterval(this.msgRecieveInterval); + }); + + this.socket.on('msg', (e) => { + clearTimeout(this.nopRecieveTimeout); + clearInterval(this.msgRecieveInterval); + this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000); + }); + + this.nopSendInterval = setInterval(() => this.sendNop(), 5000); + this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000); + this.sendNop(); + if (username) this.username = username; + this.rank = 0; + this.ChatRateLimit = new RateLimiter(this.Config.collabvm.automute.messages, this.Config.collabvm.automute.seconds); + this.ChatRateLimit.on('limit', () => this.mute(false)); + this.RenameRateLimit = new RateLimiter(3, 60); + this.RenameRateLimit.on('limit', () => this.closeConnection()); + this.LoginRateLimit = new RateLimiter(4, 3); + this.LoginRateLimit.on('limit', () => this.closeConnection()); + this.TurnRateLimit = new RateLimiter(5, 3); + this.TurnRateLimit.on('limit', () => this.closeConnection()); + this.VoteRateLimit = new RateLimiter(3, 3); + this.VoteRateLimit.on('limit', () => this.closeConnection()); + } + + assignGuestName(existingUsers: string[]): string { + var username; + do { + username = 'guest' + Utilities.Randint(10000, 99999); + } while (existingUsers.indexOf(username) !== -1); + this.username = username; + return username; + } + + sendNop() { + this.socket.send('3.nop;'); + } + + sendMsg(msg: string) { + if (!this.socket.isOpen()) return; + clearInterval(this.nopSendInterval); + this.nopSendInterval = setInterval(() => this.sendNop(), 5000); + this.socket.send(msg); + } + + private onNoMsg() { + this.sendNop(); + this.nopRecieveTimeout = setTimeout(() => { + this.closeConnection(); + }, 3000); + } + + closeConnection() { + this.socket.send(cvm.guacEncode('disconnect')); + this.socket.close(); + } + + onMsgSent() { + if (!this.Config.collabvm.automute.enabled) return; + // rate limit guest and unregistered chat messages, but not staff ones + switch (this.rank) { + case Rank.Moderator: + case Rank.Admin: + break; + + default: + this.ChatRateLimit.request(); + break; + } + } + + mute(permanent: boolean) { + this.IP.muted = true; + this.sendMsg(cvm.guacEncode('chat', '', `You have been muted${permanent ? '' : ` for ${this.Config.collabvm.tempMuteTime} seconds`}.`)); + if (!permanent) { + clearTimeout(this.IP.tempMuteExpireTimeout); + this.IP.tempMuteExpireTimeout = setTimeout(() => this.unmute(), this.Config.collabvm.tempMuteTime * 1000); + } + } + unmute() { + clearTimeout(this.IP.tempMuteExpireTimeout); + this.IP.muted = false; + this.sendMsg(cvm.guacEncode('chat', '', 'You are no longer muted.')); + } + + private banCmdArgs(arg: string): string { + return arg.replace(/\$IP/g, this.IP.address).replace(/\$NAME/g, this.username || ''); + } + + async ban() { + // Prevent the user from taking turns or chatting, in case the ban command takes a while + this.IP.muted = true; + + try { + if (Array.isArray(this.Config.collabvm.bancmd)) { + let args: string[] = this.Config.collabvm.bancmd.map((a: string) => this.banCmdArgs(a)); + if (args.length || args[0].length) { + await execa(args.shift()!, args, { stdout: process.stdout, stderr: process.stderr }); + this.kick(); + } else { + this.logger.error(`Failed to ban ${this.IP.address} (${this.username}): Empty command`); + } + } else if (typeof this.Config.collabvm.bancmd == 'string') { + let cmd: string = this.banCmdArgs(this.Config.collabvm.bancmd); + if (cmd.length) { + await execaCommand(cmd, { stdout: process.stdout, stderr: process.stderr }); + this.kick(); + } else { + this.logger.error(`Failed to ban ${this.IP.address} (${this.username}): Empty command`); + } + } + } catch (e) { + this.logger.error(`Failed to ban ${this.IP.address} (${this.username}): ${(e as ExecaSyncError).shortMessage}`); + } + } + + async kick() { + this.sendMsg('10.disconnect;'); + this.socket.close(); + } +} + +export enum Rank { + Unregistered = 0, + // After all these years + Registered = 1, + Admin = 2, + Moderator = 3, + // Giving a good gap between server only internal ranks just in case + Turn = 10 +} diff --git a/cvmts/src/Utilities.ts b/cvmts/src/Utilities.ts new file mode 100644 index 0000000..efdcf81 --- /dev/null +++ b/cvmts/src/Utilities.ts @@ -0,0 +1,54 @@ +import { Permissions } from './IConfig'; + +export function Randint(min: number, max: number) { + return Math.floor(Math.random() * (max - min) + min); +} + +export function HTMLSanitize(input: string): string { + var output = ''; + for (var i = 0; i < input.length; i++) { + switch (input[i]) { + case '<': + output += '<'; + break; + case '>': + output += '>'; + break; + case '&': + output += '&'; + break; + case '"': + output += '"'; + break; + case "'": + output += '''; + break; + case '/': + output += '/'; + break; + case '\n': + output += ' '; + break; + default: + var charcode: number = input.charCodeAt(i); + if (charcode >= 32 && charcode <= 126) output += input[i]; + break; + } + } + return output; +} + +export function MakeModPerms(modperms: Permissions): number { + var perms = 0; + if (modperms.restore) perms |= 1; + if (modperms.reboot) perms |= 2; + if (modperms.ban) perms |= 4; + if (modperms.forcevote) perms |= 8; + if (modperms.mute) perms |= 16; + if (modperms.kick) perms |= 32; + if (modperms.bypassturn) perms |= 64; + if (modperms.rename) perms |= 128; + if (modperms.grabip) perms |= 256; + if (modperms.xss) perms |= 512; + return perms; +} diff --git a/cvmts/src/VM.ts b/cvmts/src/VM.ts new file mode 100644 index 0000000..0038e9b --- /dev/null +++ b/cvmts/src/VM.ts @@ -0,0 +1,13 @@ +import { VMState } from '@computernewb/superqemu'; +import { VMDisplay } from './VMDisplay.js'; + +export default interface VM { + Start(): Promise; + Stop(): Promise; + Reboot(): Promise; + Reset(): Promise; + MonitorCommand(command: string): Promise; + GetDisplay(): VMDisplay; + GetState(): VMState; + SnapshotsSupported(): boolean; +} diff --git a/cvmts/src/VMDisplay.ts b/cvmts/src/VMDisplay.ts new file mode 100644 index 0000000..559e9b8 --- /dev/null +++ b/cvmts/src/VMDisplay.ts @@ -0,0 +1,25 @@ +import EventEmitter from 'node:events'; + +// not great but whatever +// nodejs-rfb COULD probably export them though. +export type Size = { + width: number; + height: number; +}; + +export type Rect = { + x: number; + y: number; + width: number; + height: number; +}; + +export interface VMDisplay extends EventEmitter { + Connect(): void; + Disconnect(): void; + Connected(): boolean; + Buffer(): Buffer; + Size(): Size; + MouseEvent(x: number, y: number, buttons: number): void; + KeyboardEvent(keysym: number, pressed: boolean): void; +} diff --git a/cvmts/src/VNCVM/VNCVM.ts b/cvmts/src/VNCVM/VNCVM.ts new file mode 100644 index 0000000..d2106a9 --- /dev/null +++ b/cvmts/src/VNCVM/VNCVM.ts @@ -0,0 +1,184 @@ +import EventEmitter from 'events'; +import VNCVMDef from './VNCVMDef'; +import VM from '../VM'; +import { Size, Rect, VMDisplay } from '../VMDisplay'; +import { VncClient } from '@computernewb/nodejs-rfb'; +import { BatchRects, VMState } from '@computernewb/superqemu'; +import { execaCommand } from 'execa'; +import pino from 'pino'; + +function Clamp(input: number, min: number, max: number) { + return Math.min(Math.max(input, min), max); +} + +async function Sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export default class VNCVM extends EventEmitter implements VM, VMDisplay { + def: VNCVMDef; + logger; + private displayVnc = new VncClient({ + debug: false, + fps: 60, + encodings: [VncClient.consts.encodings.raw, VncClient.consts.encodings.pseudoDesktopSize] + }); + private vncShouldReconnect: boolean = false; + + constructor(def: VNCVMDef) { + super(); + this.def = def; + // TODO: Now that we're using an actual structured logger can we please + this.logger = pino({ name: `CVMTS.VNCVM/${this.def.vncHost}:${this.def.vncPort}` }); + + this.displayVnc.on('connectTimeout', () => { + this.Reconnect(); + }); + + this.displayVnc.on('authError', () => { + this.Reconnect(); + }); + + this.displayVnc.on('disconnect', () => { + this.logger.info('Disconnected'); + this.Reconnect(); + }); + + this.displayVnc.on('closed', () => { + this.Reconnect(); + }); + + this.displayVnc.on('firstFrameUpdate', () => { + this.logger.info('Connected'); + // apparently this library is this good. + // at least it's better than the two others which exist. + this.displayVnc.changeFps(60); + this.emit('connected'); + + this.emit('resize', { width: this.displayVnc.clientWidth, height: this.displayVnc.clientHeight }); + //this.emit('rect', { x: 0, y: 0, width: this.displayVnc.clientWidth, height: this.displayVnc.clientHeight }); + this.emit('frame'); + }); + + this.displayVnc.on('desktopSizeChanged', (size: Size) => { + this.emit('resize', size); + }); + + let rects: Rect[] = []; + + this.displayVnc.on('rectUpdateProcessed', (rect: Rect) => { + rects.push(rect); + }); + + this.displayVnc.on('frameUpdated', (fb: Buffer) => { + // use the cvmts batcher + let batched = BatchRects(this.Size(), rects); + this.emit('rect', batched); + + // unbatched (watch the performace go now) + //for(let rect of rects) + // this.emit('rect', rect); + + rects = []; + + this.emit('frame'); + }); + } + + async Reset(): Promise { + if (this.def.restoreCmd) await execaCommand(this.def.restoreCmd, { shell: true }); + else { + await this.Stop(); + await Sleep(1000); + await this.Start(); + } + } + + private Reconnect() { + if (this.displayVnc.connected) return; + + if (!this.vncShouldReconnect) return; + + // TODO: this should also give up after a max tries count + // if we fail after max tries, emit a event + + this.displayVnc.connect({ + host: this.def.vncHost, + port: this.def.vncPort, + path: null + }); + } + + async Start(): Promise { + this.logger.info('Connecting'); + if (this.def.startCmd) await execaCommand(this.def.startCmd, { shell: true }); + this.Connect(); + } + + async Stop(): Promise { + this.logger.info('Disconnecting'); + this.Disconnect(); + if (this.def.stopCmd) await execaCommand(this.def.stopCmd, { shell: true }); + } + + async Reboot(): Promise { + if (this.def.rebootCmd) await execaCommand(this.def.rebootCmd, { shell: true }); + } + + async MonitorCommand(command: string): Promise { + // TODO: This can maybe run a specified command? + return 'This VM does not support monitor commands.'; + } + + GetDisplay(): VMDisplay { + return this; + } + + GetState(): VMState { + // for now! + return VMState.Started; + } + + SnapshotsSupported(): boolean { + return true; + } + + Connect(): void { + this.vncShouldReconnect = true; + this.Reconnect(); + } + + Disconnect(): void { + this.vncShouldReconnect = false; + this.displayVnc.disconnect(); + } + + Connected(): boolean { + return this.displayVnc.connected; + } + + Buffer(): Buffer { + return this.displayVnc.fb; + } + + Size(): Size { + if (!this.displayVnc.connected) + return { + width: 0, + height: 0 + }; + + return { + width: this.displayVnc.clientWidth, + height: this.displayVnc.clientHeight + }; + } + + MouseEvent(x: number, y: number, buttons: number): void { + if (this.displayVnc.connected) this.displayVnc.sendPointerEvent(Clamp(x, 0, this.displayVnc.clientWidth), Clamp(y, 0, this.displayVnc.clientHeight), buttons); + } + + KeyboardEvent(keysym: number, pressed: boolean): void { + if (this.displayVnc.connected) this.displayVnc.sendKeyEvent(keysym, pressed); + } +} diff --git a/cvmts/src/VNCVM/VNCVMDef.ts b/cvmts/src/VNCVM/VNCVMDef.ts new file mode 100644 index 0000000..395977d --- /dev/null +++ b/cvmts/src/VNCVM/VNCVMDef.ts @@ -0,0 +1,8 @@ +export default interface VNCVMDef { + vncHost: string; + vncPort: number; + startCmd: string | null; + stopCmd: string | null; + rebootCmd: string | null; + restoreCmd: string | null; +} diff --git a/cvmts/src/WebSocket/WSClient.ts b/cvmts/src/WebSocket/WSClient.ts new file mode 100644 index 0000000..c2b0cbf --- /dev/null +++ b/cvmts/src/WebSocket/WSClient.ts @@ -0,0 +1,74 @@ +import { WebSocket } from 'ws'; +import NetworkClient from '../NetworkClient.js'; +import EventEmitter from 'events'; + +export default class WSClient extends EventEmitter implements NetworkClient { + socket: WebSocket; + ip: string; + + constructor(ws: WebSocket, ip: string) { + super(); + this.socket = ws; + this.ip = ip; + this.socket.on('message', (buf: Buffer, isBinary: boolean) => { + // Close the user's connection if they send a non-string message + if (isBinary) { + this.close(); + return; + } + + this.emit('msg', buf.toString('utf-8')); + }); + + this.socket.on('close', () => { + this.emit('disconnect'); + }); + } + + isOpen(): boolean { + return this.socket.readyState === WebSocket.OPEN; + } + + getIP(): string { + return this.ip; + } + + send(msg: string): Promise { + return new Promise((res, rej) => { + if (!this.isOpen()) res(); + + this.socket.send(msg, (err) => { + if (err) { + rej(err); + return; + } + res(); + }); + }); + } + + sendBinary(msg: Uint8Array): Promise { + return new Promise((res, rej) => { + if (!this.isOpen()) res(); + + this.socket.send(msg, (err) => { + if (err) { + rej(err); + return; + } + res(); + }); + }); + } + + close(): void { + if (this.isOpen()) { + // While this seems counterintutive, do note that the WebSocket protocol + // *sends* a data frame whilist closing a connection. Therefore, if the other end + // has forcibly hung up (closed) their connection, the best way to handle that + // is to just let the inner TCP socket propegate that, which `ws` will do for us. + // Otherwise, we'll try to send data to a closed client then SIGPIPE. + this.socket.close(); + } + } +} diff --git a/cvmts/src/WebSocket/WSServer.ts b/cvmts/src/WebSocket/WSServer.ts new file mode 100644 index 0000000..1808a0d --- /dev/null +++ b/cvmts/src/WebSocket/WSServer.ts @@ -0,0 +1,143 @@ +import * as http from 'http'; +import NetworkServer from '../NetworkServer.js'; +import EventEmitter from 'events'; +import { WebSocketServer, WebSocket } from 'ws'; +import internal from 'stream'; +import IConfig from '../IConfig.js'; +import { isIP } from 'net'; +import { IPDataManager } from '../IPData.js'; +import WSClient from './WSClient.js'; +import { User } from '../User.js'; +import pino from 'pino'; + +export default class WSServer extends EventEmitter implements NetworkServer { + private httpServer: http.Server; + private wsServer: WebSocketServer; + private clients: WSClient[]; + private Config: IConfig; + private logger = pino({ name: 'CVMTS.WSServer' }); + + constructor(config: IConfig) { + super(); + this.Config = config; + this.clients = []; + this.httpServer = http.createServer(); + this.wsServer = new WebSocketServer({ noServer: true }); + this.httpServer.on('upgrade', (req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) => this.httpOnUpgrade(req, socket, head)); + this.httpServer.on('request', (req, res) => { + res.writeHead(426); + res.write('This server only accepts WebSocket connections.'); + res.end(); + }); + } + + start(): void { + this.httpServer.listen(this.Config.http.port, this.Config.http.host, () => { + this.logger.info(`WebSocket server listening on ${this.Config.http.host}:${this.Config.http.port}`); + }); + } + + stop(): void { + this.httpServer.close(); + } + + private httpOnUpgrade(req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) { + var killConnection = () => { + socket.write('HTTP/1.1 400 Bad Request\n\n400 Bad Request'); + socket.destroy(); + }; + + if (req.headers['sec-websocket-protocol'] !== 'guacamole') { + killConnection(); + return; + } + + if (this.Config.http.origin) { + // If the client is not sending an Origin header, kill the connection. + if (!req.headers.origin) { + killConnection(); + return; + } + + // Try to parse the Origin header sent by the client, if it fails, kill the connection. + var _uri; + var _host; + try { + _uri = new URL(req.headers.origin.toLowerCase()); + _host = _uri.host; + } catch { + killConnection(); + return; + } + + // detect fake origin headers + if (_uri.pathname !== '/' || _uri.search !== '') { + killConnection(); + return; + } + + // If the domain name is not in the list of allowed origins, kill the connection. + if (!this.Config.http.originAllowedDomains.includes(_host)) { + killConnection(); + return; + } + } + + let ip: string; + if (this.Config.http.proxying) { + // If the requesting IP isn't allowed to proxy, kill it + if (this.Config.http.proxyAllowedIps.indexOf(req.socket.remoteAddress!) === -1) { + killConnection(); + return; + } + // Make sure x-forwarded-for is set + if (req.headers['x-forwarded-for'] === undefined) { + killConnection(); + this.logger.error('X-Forwarded-For header not set. This is most likely a misconfiguration of your reverse proxy.'); + return; + } + try { + // Get the first IP from the X-Forwarded-For variable + ip = req.headers['x-forwarded-for']?.toString().replace(/\ /g, '').split(',')[0]; + } catch { + // If we can't get the IP, kill the connection + this.logger.error('Invalid X-Forwarded-For header. This is most likely a misconfiguration of your reverse proxy.'); + killConnection(); + return; + } + // If for some reason the IP isn't defined, kill it + if (!ip) { + killConnection(); + return; + } + // Make sure the IP is valid. If not, kill the connection. + if (!isIP(ip)) { + killConnection(); + return; + } + } else { + if (!req.socket.remoteAddress) return; + ip = req.socket.remoteAddress; + } + + this.wsServer.handleUpgrade(req, socket, head, (ws: WebSocket) => { + this.wsServer.emit('connection', ws, req); + this.onConnection(ws, req, ip); + }); + } + + private onConnection(ws: WebSocket, req: http.IncomingMessage, ip: string) { + let client = new WSClient(ws, ip); + this.clients.push(client); + let user = new User(client, IPDataManager.GetIPData(ip), this.Config); + + this.emit('connect', user); + + ws.on('error', (e) => { + this.logger.error(`${e} (caused by connection ${ip})`); + ws.close(); + }); + + this.logger.info(`New WebSocket connection from ${user.IP.address}`); + } +} diff --git a/cvmts/src/index.ts b/cvmts/src/index.ts new file mode 100644 index 0000000..001cf7c --- /dev/null +++ b/cvmts/src/index.ts @@ -0,0 +1,94 @@ +import * as toml from 'toml'; +import IConfig from './IConfig.js'; +import * as fs from 'fs'; +import CollabVMServer from './CollabVMServer.js'; + +import { QemuVM, QemuVmDefinition } from '@computernewb/superqemu'; + +import AuthManager from './AuthManager.js'; +import WSServer from './WebSocket/WSServer.js'; +import { User } from './User.js'; +import TCPServer from './TCP/TCPServer.js'; +import VM from './VM.js'; +import VNCVM from './VNCVM/VNCVM.js'; +import GeoIPDownloader from './GeoIPDownloader.js'; +import pino from 'pino'; + +let logger = pino(); + +logger.info('CollabVM Server starting up'); + +// Parse the config file + +let Config: IConfig; + +if (!fs.existsSync('config.toml')) { + logger.error('Fatal error: Config.toml not found. Please copy config.example.toml and fill out fields'); + process.exit(1); +} +try { + var configRaw = fs.readFileSync('config.toml').toString(); + Config = toml.parse(configRaw); +} catch (e) { + logger.error('Fatal error: Failed to read or parse the config file: {0}', (e as Error).message); + process.exit(1); +} + +let exiting = false; +let VM: VM; + +async function stop() { + if (exiting) return; + exiting = true; + await VM.Stop(); + process.exit(0); +} + +async function start() { + let geoipReader = null; + if (Config.geoip.enabled) { + let downloader = new GeoIPDownloader(Config.geoip.directory, Config.geoip.accountID, Config.geoip.licenseKey); + geoipReader = await downloader.getGeoIPReader(); + } + // Init the auth manager if enabled + let auth = Config.auth.enabled ? new AuthManager(Config.auth.apiEndpoint, Config.auth.secretKey) : null; + switch (Config.vm.type) { + case 'qemu': { + // Fire up the VM + let def: QemuVmDefinition = { + id: Config.collabvm.node, + command: Config.qemu.qemuArgs, + snapshot: Config.qemu.snapshots + }; + + VM = new QemuVM(def); + break; + } + case 'vncvm': { + VM = new VNCVM(Config.vncvm); + break; + } + default: { + logger.error(`Invalid VM type in config: ${Config.vm.type}`); + process.exit(1); + return; + } + } + process.on('SIGINT', async () => await stop()); + process.on('SIGTERM', async () => await stop()); + + await VM.Start(); + // Start up the server + var CVM = new CollabVMServer(Config, VM, auth, geoipReader); + + var WS = new WSServer(Config); + WS.on('connect', (client: User) => CVM.addUser(client)); + WS.start(); + + if (Config.tcp.enabled) { + var TCP = new TCPServer(Config); + TCP.on('connect', (client: User) => CVM.addUser(client)); + TCP.start(); + } +} +start(); diff --git a/cvmts/tsconfig.json b/cvmts/tsconfig.json new file mode 100644 index 0000000..baa2339 --- /dev/null +++ b/cvmts/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "include": [ "src/**/*" ], + "compilerOptions": { + "resolveJsonModule": true, + } +} diff --git a/package.json b/package.json index 07ec309..c1a3c67 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,25 @@ { - "name": "collabvm1.ts", - "version": "1.0.0", - "description": "replacement for collabvm 1.2.11 because the old one :boom:", - "main": "build/index.js", - "scripts": { - "build": "tsc", - "serve": "node build/index.js" - }, - "author": "Elijah R", - "license": "GPL-3.0", - "dependencies": { - "@types/node": "^20.6.0", - "@types/sharp": "^0.31.1", - "@types/ws": "^8.5.5", - "async-mutex": "^0.4.0", - "canvas": "^2.11.2", - "execa": "^8.0.1", - "jimp": "^0.22.10", - "mnemonist": "^0.39.5", - "rfb2": "github:elijahr2411/node-rfb2", - "toml": "^3.0.0", - "typescript": "^5.2.2", - "ws": "^8.14.1" - }, - "type": "module" + "name": "cvmts-repo", + "workspaces": [ + "shared", + "cvm-rs", + "cvmts", + "collab-vm-1.2-binary-protocol" + ], + "devDependencies": { + "@parcel/packager-ts": "2.12.0", + "@parcel/transformer-sass": "2.12.0", + "@parcel/transformer-typescript-types": "2.12.0", + "@types/node": "^20.14.10", + "just-install": "^2.0.1", + "parcel": "^2.12.0", + "prettier": "^3.2.5", + "typescript": "^5.4.4" + }, + "packageManager": "yarn@4.1.1", + "scripts": { + "build": "just", + "serve": "node cvmts/dist/index.js", + "clean": "rm -rf .parcel-cache .yarn **/node_modules **/dist cvm-rs/target cvm-rs/index.node" + } } diff --git a/src/AuthManager.ts b/src/AuthManager.ts deleted file mode 100644 index 69fc01e..0000000 --- a/src/AuthManager.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Rank, User } from "./User.js"; -import log from "./log.js"; - -export default class AuthManager { - apiEndpoint : string; - secretKey : string; - constructor(apiEndpoint : string, secretKey : string) { - this.apiEndpoint = apiEndpoint; - this.secretKey = secretKey; - } - - Authenticate(token : string, user : User) { - return new Promise(async res => { - var response = await fetch(this.apiEndpoint + "/api/v1/join", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - secretKey: this.secretKey, - sessionToken: token, - ip: user.IP.address - }) - }); - var json = await response.json() as JoinResponse; - if (!json.success) { - log("FATAL", `Failed to query auth server: ${json.error}`); - process.exit(1); - } - res(json); - }); - } -} - -interface JoinResponse { - success : boolean; - clientSuccess : boolean; - error : string | undefined; - username : string | undefined; - rank : Rank; -} \ No newline at end of file diff --git a/src/Framebuffer.ts b/src/Framebuffer.ts deleted file mode 100644 index 33b1dda..0000000 --- a/src/Framebuffer.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Mutex } from "async-mutex"; - -export default class Framebuffer { - fb : Buffer; - private writemutex : Mutex; - size : {height : number, width : number}; - constructor() { - this.fb = Buffer.alloc(1); - this.size = {height: 0, width: 0}; - this.writemutex = new Mutex(); - } - setSize(w : number, h : number) { - var size = h * w * 4; - this.size.height = h; - this.size.width = w; - this.fb = Buffer.alloc(size); - } - loadDirtyRect(rect : Buffer, x : number, y : number, width : number, height : number) : Promise { - if (this.fb.length < rect.length) - throw new Error("Dirty rect larger than framebuffer (did you forget to set the size?)"); - return this.writemutex.runExclusive(() => { - return new Promise((res, rej) => { - var byteswritten = 0; - for (var i = 0; i < height; i++) { - byteswritten += rect.copy(this.fb, 4 * ((y + i) * this.size.width + x), byteswritten, byteswritten + (width * 4)); - } - res(); - }) - }); - } - getFb() : Promise { - return new Promise(async (res, rej) => { - var v = await this.writemutex.runExclusive(() => { - return new Promise((reso, reje) => { - var buff = Buffer.alloc(this.fb.length); - this.fb.copy(buff); - reso(buff); - }); - }); - res(v); - }) - } - -} \ No newline at end of file diff --git a/src/IConfig.ts b/src/IConfig.ts deleted file mode 100644 index d4d920c..0000000 --- a/src/IConfig.ts +++ /dev/null @@ -1,71 +0,0 @@ -export default interface IConfig { - http : { - host : string; - port : number; - proxying : boolean; - proxyAllowedIps : string[]; - origin : boolean; - originAllowedDomains : string[]; - maxConnections: number; - }; - auth : { - enabled : boolean; - apiEndpoint : string; - secretKey : string; - guestPermissions : { - chat : boolean; - turn : boolean; - callForReset : boolean; - vote : boolean; - } - } - vm : { - qemuArgs : string; - vncPort : number; - snapshots : boolean; - qmpHost : string | null; - qmpPort : number | null; - qmpSockDir : string | null; - }; - collabvm : { - node : string; - displayname : string; - motd : string; - bancmd : string | string[]; - moderatorEnabled : boolean; - usernameblacklist : string[]; - maxChatLength : number; - maxChatHistoryLength : number; - turnlimit : { - enabled: boolean, - maximum: number; - }; - automute : { - enabled: boolean; - seconds: number; - messages: number; - }; - tempMuteTime : number; - turnTime : number; - voteTime : number; - voteCooldown: number; - adminpass : string; - modpass : string; - turnwhitelist : boolean; - turnpass : string; - moderatorPermissions : Permissions; - }; -}; - -export interface Permissions { - restore : boolean; - reboot : boolean; - ban : boolean; - forcevote : boolean; - mute : boolean; - kick : boolean; - bypassturn : boolean; - rename : boolean; - grabip : boolean; - xss : boolean; -} \ No newline at end of file diff --git a/src/IPData.ts b/src/IPData.ts deleted file mode 100644 index 9a7c329..0000000 --- a/src/IPData.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class IPData { - tempMuteExpireTimeout? : NodeJS.Timeout; - muted: Boolean; - vote: boolean | null; - address: string; - - constructor(address: string) { - this.address = address; - this.muted = false; - this.vote = null; - } -} \ No newline at end of file diff --git a/src/QEMUVM.ts b/src/QEMUVM.ts deleted file mode 100644 index 88dc398..0000000 --- a/src/QEMUVM.ts +++ /dev/null @@ -1,254 +0,0 @@ -import IConfig from "./IConfig.js"; -import * as rfb from 'rfb2'; -import * as fs from 'fs'; -import { ExecaChildProcess, execaCommand } from "execa"; -import QMPClient from "./QMPClient.js"; -import BatchRects from "./RectBatcher.js"; -import { createCanvas, Canvas, CanvasRenderingContext2D, createImageData } from "canvas"; -import { Mutex } from "async-mutex"; -import log from "./log.js"; -import VM from "./VM.js"; - -export default class QEMUVM extends VM { - vnc? : rfb.RfbClient; - vncPort : number; - framebuffer : Canvas; - framebufferCtx : CanvasRenderingContext2D; - qmpSock : string; - qmpType: string; - qmpClient : QMPClient; - qemuCmd : string; - qemuProcess? : ExecaChildProcess; - qmpErrorLevel : number; - vncErrorLevel : number; - processRestartErrorLevel : number; - expectedExit : boolean; - vncOpen : boolean; - vncUpdateInterval? : NodeJS.Timeout; - rects : {height:number,width:number,x:number,y:number,data:Buffer}[]; - rectMutex : Mutex; - - vncReconnectTimeout? : NodeJS.Timeout; - qmpReconnectTimeout? : NodeJS.Timeout; - qemuRestartTimeout? : NodeJS.Timeout; - - constructor(Config : IConfig) { - super(); - if (Config.vm.vncPort < 5900) { - log("FATAL", "VNC port must be 5900 or higher") - process.exit(1); - } - Config.vm.qmpSockDir == null ? this.qmpType = "tcp:" : this.qmpType = "unix:"; - if(this.qmpType == "tcp:") { - this.qmpSock = `${Config.vm.qmpHost}:${Config.vm.qmpPort}`; - }else{ - this.qmpSock = `${Config.vm.qmpSockDir}collab-vm-qmp-${Config.collabvm.node}.sock`; - } - this.vncPort = Config.vm.vncPort; - this.qemuCmd = `${Config.vm.qemuArgs} -no-shutdown -vnc 127.0.0.1:${this.vncPort - 5900} -qmp ${this.qmpType}${this.qmpSock},server,nowait`; - if (Config.vm.snapshots) this.qemuCmd += " -snapshot" - this.qmpErrorLevel = 0; - this.vncErrorLevel = 0; - this.vncOpen = true; - this.rects = []; - this.rectMutex = new Mutex(); - this.framebuffer = createCanvas(1, 1); - this.framebufferCtx = this.framebuffer.getContext("2d"); - this.processRestartErrorLevel = 0; - this.expectedExit = false; - this.qmpClient = new QMPClient(this.qmpSock, this.qmpType); - this.qmpClient.on('connected', () => this.qmpConnected()); - this.qmpClient.on('close', () => this.qmpClosed()); - } - - Start() : Promise { - return new Promise(async (res, rej) => { - if (fs.existsSync(this.qmpSock)) - try { - fs.unlinkSync(this.qmpSock); - } catch (e) { - log("ERROR", `Failed to delete existing socket: ${e}`); - process.exit(-1); - } - this.qemuProcess = execaCommand(this.qemuCmd); - this.qemuProcess.catch(() => false); - this.qemuProcess.stderr?.on('data', (d) => log("ERROR", `QEMU sent to stderr: ${d.toString()}`)); - this.qemuProcess.once('spawn', () => { - setTimeout(async () => { - await this.qmpClient.connect(); - }, 2000) - }); - this.qemuProcess.once('exit', () => { - if (this.expectedExit) return; - clearTimeout(this.qmpReconnectTimeout); - clearTimeout(this.vncReconnectTimeout); - this.processRestartErrorLevel++; - if (this.processRestartErrorLevel > 4) { - log("FATAL", "QEMU failed to launch 5 times."); - process.exit(-1); - } - log("WARN", "QEMU exited unexpectedly, retrying in 3 seconds"); - this.qmpClient.disconnect(); - this.vnc?.end(); - this.qemuRestartTimeout = setTimeout(() => this.Start(), 3000); - }); - this.qemuProcess.on('error', () => false); - this.once('vncconnect', () => res()); - }); - } - - private qmpConnected() { - this.qmpErrorLevel = 0; - this.processRestartErrorLevel = 0; - log("INFO", "QMP Connected"); - setTimeout(() => this.startVNC(), 1000); - } - - private startVNC() { - this.vnc = rfb.createConnection({ - host: "127.0.0.1", - port: this.vncPort, - }); - this.vnc.on("close", () => this.vncClosed()); - this.vnc.on("connect", () => this.vncConnected()); - this.vnc.on("rect", (r) => this.onVNCRect(r)); - this.vnc.on("resize", (s) => this.onVNCSize(s)); - } - - public getSize() { - if (!this.vnc) return {height:0,width:0}; - return {height: this.vnc.height, width: this.vnc.width} - } - - private qmpClosed() { - if (this.expectedExit) return; - this.qmpErrorLevel++; - if (this.qmpErrorLevel > 4) { - log("FATAL", "Failed to connect to QMP after 5 attempts"); - process.exit(1); - } - log("ERROR", "Failed to connect to QMP, retrying in 3 seconds."); - this.qmpReconnectTimeout = setTimeout(() => this.qmpClient.connect(), 3000); - } - - private vncClosed() { - this.vncOpen = false; - if (this.expectedExit) return; - this.vncErrorLevel++; - if (this.vncErrorLevel > 4) { - log("FATAL", "Failed to connect to VNC after 5 attempts.") - process.exit(1); - } - try { - this.vnc?.end(); - } catch {}; - log("ERROR", "Failed to connect to VNC, retrying in 3 seconds"); - this.vncReconnectTimeout = setTimeout(() => this.startVNC(), 3000); - } - - private vncConnected() { - this.vncOpen = true; - this.emit('vncconnect'); - log("INFO", "VNC Connected"); - this.vncErrorLevel = 0; - - this.onVNCSize({height: this.vnc!.height, width: this.vnc!.width}); - this.vncUpdateInterval = setInterval(() => this.SendRects(), 33); - } - private onVNCRect(rect : any) { - return this.rectMutex.runExclusive(async () => { - return new Promise(async (res, rej) => { - var buff = Buffer.alloc(rect.height * rect.width * 4) - var offset = 0; - for (var i = 0; i < rect.data.length; i += 4) { - buff[offset++] = rect.data[i + 2]; - buff[offset++] = rect.data[i + 1]; - buff[offset++] = rect.data[i]; - buff[offset++] = 255; - } - var imgdata = createImageData(Uint8ClampedArray.from(buff), rect.width, rect.height); - this.framebufferCtx.putImageData(imgdata, rect.x, rect.y); - this.rects.push({ - x: rect.x, - y: rect.y, - height: rect.height, - width: rect.width, - data: buff, - }); - if (!this.vnc) throw new Error(); - if (this.vncOpen) - this.vnc.requestUpdate(true, 0, 0, this.vnc.height, this.vnc.width); - res(); - }) - }); - } - - SendRects() { - if (!this.vnc || this.rects.length < 1) return; - return this.rectMutex.runExclusive(() => { - return new Promise(async (res, rej) => { - var rect = await BatchRects(this.framebuffer, [...this.rects]); - this.rects = []; - this.emit('dirtyrect', rect.data, rect.x, rect.y); - res(); - }); - }) - } - - private onVNCSize(size : any) { - if (this.framebuffer.height !== size.height) this.framebuffer.height = size.height; - if (this.framebuffer.width !== size.width) this.framebuffer.width = size.width; - this.emit("size", {height: size.height, width: size.width}); - } - - Reboot() : Promise { - return new Promise(async (res, rej) => { - if (this.expectedExit) {res(); return;} - res(await this.qmpClient.reboot()); - }); - } - - async Restore() { - if (this.expectedExit) return; - await this.Stop(); - this.expectedExit = false; - this.Start(); - } - - Stop() : Promise { - return new Promise(async (res, rej) => { - if (this.expectedExit) {res(); return;} - if (!this.qemuProcess) throw new Error("VM was not running"); - this.expectedExit = true; - this.vncOpen = false; - this.vnc?.end(); - clearInterval(this.vncUpdateInterval); - var killTimeout = setTimeout(() => { - log("WARN", "Force killing QEMU after 10 seconds of waiting for shutdown"); - this.qemuProcess?.kill(9); - }, 10000); - var closep = new Promise(async (reso, reje) => { - this.qemuProcess?.once('exit', () => reso()); - await this.qmpClient.execute({ "execute": "quit" }); - }); - var qmpclosep = new Promise((reso, rej) => { - this.qmpClient.once('close', () => reso()); - }); - await Promise.all([closep, qmpclosep]); - clearTimeout(killTimeout); - res(); - }) - } - - public pointerEvent(x: number, y: number, mask: number) { - if (!this.vnc) throw new Error("VNC was not instantiated."); - this.vnc.pointerEvent(x, y, mask); - } - public acceptingInput(): boolean { - return this.vncOpen; - } - public keyEvent(keysym: number, down: boolean): void { - if (!this.vnc) throw new Error("VNC was not instantiated."); - this.vnc.keyEvent(keysym, down ? 1 : 0); - } -} diff --git a/src/QMPClient.ts b/src/QMPClient.ts deleted file mode 100644 index 9c4a357..0000000 --- a/src/QMPClient.ts +++ /dev/null @@ -1,152 +0,0 @@ -import EventEmitter from "events"; -import { Socket } from "net"; -import { Mutex } from "async-mutex"; -import log from "./log.js"; -import { EOL } from "os"; - -export default class QMPClient extends EventEmitter { - socketfile : string; - sockettype: string; - socket : Socket; - connected : boolean; - sentConnected : boolean; - cmdMutex : Mutex; // So command outputs don't get mixed up - constructor(socketfile : string, sockettype: string) { - super(); - this.sockettype = sockettype; - this.socketfile = socketfile; - this.socket = new Socket(); - this.connected = false; - this.sentConnected = false; - this.cmdMutex = new Mutex(); - } - connect() : Promise { - return new Promise((res, rej) => { - if (this.connected) {res(); return;} - try { - if(this.sockettype == "tcp:") { - let _sock = this.socketfile.split(':'); - this.socket.connect(parseInt(_sock[1]), _sock[0]); - }else{ - this.socket.connect(this.socketfile); - } - } catch (e) { - this.onClose(); - } - this.connected = true; - this.socket.on('error', () => false); // Disable throwing if QMP errors - this.socket.on('data', (data) => { - data.toString().split(EOL).forEach(instr => this.onData(instr)); - }); - this.socket.on('close', () => this.onClose()); - this.once('connected', () => {res();}); - }) - } - - disconnect() { - this.connected = false; - this.socket.destroy(); - } - - private async onData(data : string) { - let msg; - - try { - msg = JSON.parse(data); - } catch { - return; - } - - if (msg.QMP !== undefined) { - if (this.sentConnected) - return; - - await this.execute({ execute: "qmp_capabilities" }); - - this.emit('connected'); - this.sentConnected = true; - } - if (msg.return !== undefined && Object.keys(msg.return).length) - this.emit("qmpreturn", msg.return); - else if(msg.event !== undefined) { - switch(msg.event) { - case "STOP": - { - log("INFO", "The VM was shut down, restarting..."); - this.reboot(); - break; - } - case "RESET": - { - log("INFO", "QEMU reset event occured"); - this.resume(); - break; - }; - default: break; - } - }else - // for now just return an empty string. - // This is a giant hack but avoids a deadlock - this.emit("qmpreturn", ''); - } - - private onClose() { - this.connected = false; - this.sentConnected = false; - - if (this.socket.readyState === 'open') - this.socket.destroy(); - - this.cmdMutex.cancel(); - this.cmdMutex.release(); - this.socket = new Socket(); - this.emit('close'); - } - - async reboot() { - if (!this.connected) - return; - - await this.execute({"execute": "system_reset"}); - } - - async resume() { - if (!this.connected) - return; - - await this.execute({"execute": "cont"}); - } - - async ExitQEMU() { - if (!this.connected) - return; - - await this.execute({"execute": "quit"}); - } - - execute(args : object) { - return new Promise(async (res, rej) => { - var result:any; - try { - result = await this.cmdMutex.runExclusive(() => { - // I kinda hate having two promises but IDK how else to do it /shrug - return new Promise((reso, reje) => { - this.once('qmpreturn', (e) => { - reso(e); - }); - this.socket.write(JSON.stringify(args)); - }); - }); - } catch { - res({}); - } - res(result); - }); - } - - runMonitorCmd(command : string) { - return new Promise(async (res, rej) => { - res(await this.execute({execute: "human-monitor-command", arguments: {"command-line": command}})); - }); - } -} diff --git a/src/RateLimiter.ts b/src/RateLimiter.ts deleted file mode 100644 index 32399ea..0000000 --- a/src/RateLimiter.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { EventEmitter } from "events"; - -// Class to ratelimit a resource (chatting, logging in, etc) -export default class RateLimiter extends EventEmitter { - private limit : number; - private interval : number; - private requestCount : number; - private limiter? : NodeJS.Timeout; - private limiterSet : boolean; - constructor(limit : number, interval : number) { - super(); - this.limit = limit; - this.interval = interval; - this.requestCount = 0; - this.limiterSet = false; - } - // Return value is whether or not the action should be continued - request() : boolean { - this.requestCount++; - if (this.requestCount === this.limit) { - this.emit('limit'); - clearTimeout(this.limiter); - this.limiterSet = false; - this.requestCount = 0; - return false; - } - if (!this.limiterSet) { - this.limiter = setTimeout(() => { - this.limiterSet = false; - this.requestCount = 0; - }, this.interval * 1000); - this.limiterSet = true; - } - return true; - } -} \ No newline at end of file diff --git a/src/RectBatcher.ts b/src/RectBatcher.ts deleted file mode 100644 index 46242d4..0000000 --- a/src/RectBatcher.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Canvas, createCanvas, createImageData } from "canvas"; - -export default async function BatchRects(fb : Canvas, rects : {height:number,width:number,x:number,y:number,data:Buffer}[]) : Promise<{x:number,y:number,data:Canvas}> { - var mergedX = fb.width; - var mergedY = fb.height; - var mergedHeight = 0; - var mergedWidth = 0; - rects.forEach((r) => { - if (r.x < mergedX) mergedX = r.x; - if (r.y < mergedY) mergedY = r.y; - }); - rects.forEach(r => { - if (((r.height + r.y) - mergedY) > mergedHeight) mergedHeight = (r.height + r.y) - mergedY; - if (((r.width + r.x) - mergedX) > mergedWidth) mergedWidth = (r.width + r.x) - mergedX; - }); - var rect = createCanvas(mergedWidth, mergedHeight); - var ctx = rect.getContext("2d"); - ctx.drawImage(fb, mergedX, mergedY, mergedWidth, mergedHeight, 0, 0, mergedWidth, mergedHeight); - for (const r of rects) { - var id = createImageData(Uint8ClampedArray.from(r.data), r.width, r.height); - ctx.putImageData(id, r.x - mergedX, r.y - mergedY); - } - return { - data: rect, - x: mergedX, - y: mergedY, - } -} \ No newline at end of file diff --git a/src/User.ts b/src/User.ts deleted file mode 100644 index 85acb8c..0000000 --- a/src/User.ts +++ /dev/null @@ -1,158 +0,0 @@ -import * as Utilities from './Utilities.js'; -import * as guacutils from './guacutils.js'; -import {WebSocket} from 'ws'; -import {IPData} from './IPData.js'; -import IConfig from './IConfig.js'; -import RateLimiter from './RateLimiter.js'; -import { execa, execaCommand, ExecaSyncError } from 'execa'; -import log from './log.js'; - -export class User { - socket : WebSocket; - nopSendInterval : NodeJS.Timeout; - msgRecieveInterval : NodeJS.Timeout; - nopRecieveTimeout? : NodeJS.Timeout; - username? : string; - connectedToNode : boolean; - viewMode : number; - rank : Rank; - msgsSent : number; - Config : IConfig; - IP : IPData; - // Rate limiters - ChatRateLimit : RateLimiter; - LoginRateLimit : RateLimiter; - RenameRateLimit : RateLimiter; - TurnRateLimit : RateLimiter; - VoteRateLimit : RateLimiter; - constructor(ws : WebSocket, ip : IPData, config : IConfig, username? : string, node? : string) { - this.IP = ip; - this.connectedToNode = false; - this.viewMode = -1; - this.Config = config; - this.socket = ws; - this.msgsSent = 0; - this.socket.on('close', () => { - clearInterval(this.nopSendInterval); - }); - this.socket.on('message', (e) => { - clearTimeout(this.nopRecieveTimeout); - clearInterval(this.msgRecieveInterval); - this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000); - }) - this.nopSendInterval = setInterval(() => this.sendNop(), 5000); - this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000); - this.sendNop(); - if (username) this.username = username; - this.rank = 0; - this.ChatRateLimit = new RateLimiter(this.Config.collabvm.automute.messages, this.Config.collabvm.automute.seconds); - this.ChatRateLimit.on('limit', () => this.mute(false)); - this.RenameRateLimit = new RateLimiter(3, 60); - this.RenameRateLimit.on('limit', () => this.closeConnection()); - this.LoginRateLimit = new RateLimiter(4, 3); - this.LoginRateLimit.on('limit', () => this.closeConnection()); - this.TurnRateLimit = new RateLimiter(5, 3); - this.TurnRateLimit.on('limit', () => this.closeConnection()); - this.VoteRateLimit = new RateLimiter(3, 3); - this.VoteRateLimit.on('limit', () => this.closeConnection()); - } - assignGuestName(existingUsers : string[]) : string { - var username; - do { - username = "guest" + Utilities.Randint(10000, 99999); - } while (existingUsers.indexOf(username) !== -1); - this.username = username; - return username; - } - sendNop() { - this.socket.send("3.nop;"); - } - sendMsg(msg : string | Buffer) { - if (this.socket.readyState !== this.socket.OPEN) return; - clearInterval(this.nopSendInterval); - this.nopSendInterval = setInterval(() => this.sendNop(), 5000); - this.socket.send(msg); - } - private onNoMsg() { - this.sendNop(); - this.nopRecieveTimeout = setTimeout(() => { - this.closeConnection(); - }, 3000); - } - closeConnection() { - this.socket.send(guacutils.encode("disconnect")); - this.socket.close(); - } - onMsgSent() { - if (!this.Config.collabvm.automute.enabled) return; - // rate limit guest and unregistered chat messages, but not staff ones - switch(this.rank) { - case Rank.Moderator: - case Rank.Admin: - break; - - default: - this.ChatRateLimit.request(); - break; - } - } - mute(permanent : boolean) { - this.IP.muted = true; - this.sendMsg(guacutils.encode("chat", "", `You have been muted${permanent ? "" : ` for ${this.Config.collabvm.tempMuteTime} seconds`}.`)); - if (!permanent) { - clearTimeout(this.IP.tempMuteExpireTimeout); - this.IP.tempMuteExpireTimeout = setTimeout(() => this.unmute(), this.Config.collabvm.tempMuteTime * 1000); - } - } - unmute() { - clearTimeout(this.IP.tempMuteExpireTimeout); - this.IP.muted = false; - this.sendMsg(guacutils.encode("chat", "", "You are no longer muted.")); - } - - private banCmdArgs(arg: string) : string { - return arg.replace(/\$IP/g, this.IP.address).replace(/\$NAME/g, this.username || ""); - } - - async ban() { - // Prevent the user from taking turns or chatting, in case the ban command takes a while - this.IP.muted = true; - - try { - if (Array.isArray(this.Config.collabvm.bancmd)) { - let args: string[] = this.Config.collabvm.bancmd.map((a: string) => this.banCmdArgs(a)); - if (args.length || args[0].length) { - await execa(args.shift()!, args, {stdout: process.stdout, stderr: process.stderr}); - this.kick(); - } else { - log("ERROR", `Failed to ban ${this.IP.address} (${this.username}): Empty command`); - } - } else if (typeof this.Config.collabvm.bancmd == "string") { - let cmd: string = this.banCmdArgs(this.Config.collabvm.bancmd); - if (cmd.length) { - await execaCommand(cmd, {stdout: process.stdout, stderr: process.stderr}); - this.kick(); - } else { - log("ERROR", `Failed to ban ${this.IP.address} (${this.username}): Empty command`); - } - } - } catch (e) { - log("ERROR", `Failed to ban ${this.IP.address} (${this.username}): ${(e as ExecaSyncError).shortMessage}`); - } - } - - async kick() { - this.sendMsg("10.disconnect;"); - this.socket.close(); - } -} - -export enum Rank { - Unregistered = 0, - // After all these years - Registered = 1, - Admin = 2, - Moderator = 3, - // Giving a good gap between server only internal ranks just in case - Turn = 10, -} diff --git a/src/Utilities.ts b/src/Utilities.ts deleted file mode 100644 index ac67fc2..0000000 --- a/src/Utilities.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Permissions } from "./IConfig"; - -export function Randint(min : number, max : number) { - return Math.floor((Math.random() * (max - min)) + min); -} -export function HTMLSanitize(input : string) : string { - var output = ""; - for (var i = 0; i < input.length; i++) { - switch (input[i]) { - case "<": - output += "<" - break; - case ">": - output += ">" - break; - case "&": - output += "&" - break; - case "\"": - output += """ - break; - case "'": - output += "'"; - break; - case "/": - output += "/"; - break; - case "\n": - output += " "; - break; - default: - var charcode : number = input.charCodeAt(i); - if (charcode >= 32 && charcode <= 126) - output += input[i]; - break; - } - } - return output; -} - -export function MakeModPerms(modperms : Permissions) : number { - var perms = 0; - if (modperms.restore) perms |= 1; - if (modperms.reboot) perms |= 2; - if (modperms.ban) perms |= 4; - if (modperms.forcevote) perms |= 8; - if (modperms.mute) perms |= 16; - if (modperms.kick) perms |= 32; - if (modperms.bypassturn) perms |= 64; - if (modperms.rename) perms |= 128; - if (modperms.grabip) perms |= 256; - if (modperms.xss) perms |= 512; - return perms; -} \ No newline at end of file diff --git a/src/VM.ts b/src/VM.ts deleted file mode 100644 index b06113c..0000000 --- a/src/VM.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Canvas } from "canvas"; -import EventEmitter from "events"; - -export default abstract class VM extends EventEmitter { - public abstract getSize() : {height:number;width:number;}; - public abstract get framebuffer() : Canvas; - public abstract pointerEvent(x : number, y : number, mask : number) : void; - public abstract acceptingInput() : boolean; - public abstract keyEvent(keysym : number, down : boolean) : void; - public abstract Restore() : void; - public abstract Reboot() : Promise; -} \ No newline at end of file diff --git a/src/WSServer.ts b/src/WSServer.ts deleted file mode 100644 index 6155b00..0000000 --- a/src/WSServer.ts +++ /dev/null @@ -1,917 +0,0 @@ -import {WebSocketServer, WebSocket} from 'ws'; -import * as http from 'http'; -import IConfig from './IConfig.js'; -import internal from 'stream'; -import * as Utilities from './Utilities.js'; -import { User, Rank } from './User.js'; -import * as guacutils from './guacutils.js'; -// I hate that you have to do it like this -import CircularBuffer from 'mnemonist/circular-buffer.js'; -import Queue from 'mnemonist/queue.js'; -import { createHash } from 'crypto'; -import { isIP } from 'net'; -import QEMUVM from './QEMUVM.js'; -import { Canvas, createCanvas } from 'canvas'; -import { IPData } from './IPData.js'; -import { readFileSync } from 'fs'; -import log from './log.js'; -import VM from './VM.js'; -import { fileURLToPath } from 'url'; -import path from 'path'; -import AuthManager from './AuthManager.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -export default class WSServer { - private Config : IConfig; - private server : http.Server; - private socket : WebSocketServer; - private clients : User[]; - private ips : IPData[]; - private ChatHistory : CircularBuffer<{user:string,msg:string}> - private TurnQueue : Queue; - // Time remaining on the current turn - private TurnTime : number; - // Interval to keep track of the current turn time - private TurnInterval? : NodeJS.Timeout; - // If a reset vote is in progress - private voteInProgress : boolean; - // Interval to keep track of vote resets - private voteInterval? : NodeJS.Timeout; - // How much time is left on the vote - private voteTime : number; - // How much time until another reset vote can be cast - private voteCooldown : number; - // Interval to keep track - private voteCooldownInterval? : NodeJS.Timeout; - // Completely disable turns - private turnsAllowed : boolean; - // Hide the screen - private screenHidden : boolean; - // base64 image to show when the screen is hidden - private screenHiddenImg : string; - private screenHiddenThumb : string; - // Indefinite turn - private indefiniteTurn : User | null; - private ModPerms : number; - private VM : VM; - - // Authentication manager - private auth : AuthManager | null; - - constructor(config : IConfig, vm : VM, auth : AuthManager | null) { - this.Config = config; - this.ChatHistory = new CircularBuffer<{user:string,msg:string}>(Array, this.Config.collabvm.maxChatHistoryLength); - this.TurnQueue = new Queue(); - this.TurnTime = 0; - this.clients = []; - this.ips = []; - this.voteInProgress = false; - this.voteTime = 0; - this.voteCooldown = 0; - this.turnsAllowed = true; - this.screenHidden = false; - this.screenHiddenImg = readFileSync(__dirname + "/../assets/screenhidden.jpeg").toString("base64"); - this.screenHiddenThumb = readFileSync(__dirname + "/../assets/screenhiddenthumb.jpeg").toString("base64"); - - this.indefiniteTurn = null; - this.ModPerms = Utilities.MakeModPerms(this.Config.collabvm.moderatorPermissions); - this.server = http.createServer(); - this.socket = new WebSocketServer({noServer: true}); - this.server.on('upgrade', (req : http.IncomingMessage, socket : internal.Duplex, head : Buffer) => this.httpOnUpgrade(req, socket, head)); - this.server.on('request', (req, res) => { - res.writeHead(426); - res.write("This server only accepts WebSocket connections."); - res.end(); - }); - var initSize = vm.getSize(); - this.newsize(initSize); - this.VM = vm; - this.VM.on("dirtyrect", (j, x, y) => this.newrect(j, x, y)); - this.VM.on("size", (s) => this.newsize(s)); - // authentication manager - this.auth = auth; - } - - listen() { - this.server.listen(this.Config.http.port, this.Config.http.host); - } - - private httpOnUpgrade(req : http.IncomingMessage, socket : internal.Duplex, head : Buffer) { - var killConnection = () => { - socket.write("HTTP/1.1 400 Bad Request\n\n400 Bad Request"); - socket.destroy(); - } - - if (req.headers['sec-websocket-protocol'] !== "guacamole") { - killConnection(); - return; - } - - if (this.Config.http.origin) { - // If the client is not sending an Origin header, kill the connection. - if(!req.headers.origin) { - killConnection(); - return; - } - - // Try to parse the Origin header sent by the client, if it fails, kill the connection. - var _uri; - var _host; - try { - _uri = new URL(req.headers.origin.toLowerCase()); - _host = _uri.host; - } catch { - killConnection(); - return; - } - - // detect fake origin headers - if (_uri.pathname !== "/" || _uri.search !== "") { - killConnection(); - return; - } - - // If the domain name is not in the list of allowed origins, kill the connection. - if(!this.Config.http.originAllowedDomains.includes(_host)) { - killConnection(); - return; - } - } - - let ip: string; - if (this.Config.http.proxying) { - // If the requesting IP isn't allowed to proxy, kill it - if (this.Config.http.proxyAllowedIps.indexOf(req.socket.remoteAddress!) === -1) { - killConnection(); - return; - } - // Make sure x-forwarded-for is set - if (req.headers["x-forwarded-for"] === undefined) { - killConnection(); - return; - } - try { - // Get the first IP from the X-Forwarded-For variable - ip = req.headers["x-forwarded-for"]?.toString().replace(/\ /g, "").split(",")[0]; - } catch { - // If we can't get the IP, kill the connection - killConnection(); - return; - } - // If for some reason the IP isn't defined, kill it - if (!ip) { - killConnection(); - return; - } - // Make sure the IP is valid. If not, kill the connection. - if (!isIP(ip)) { - killConnection(); - return; - } - } else { - if (!req.socket.remoteAddress) return; - ip = req.socket.remoteAddress; - } - - // Get the amount of active connections coming from the requesting IP. - let connections = this.clients.filter(client => client.IP.address == ip); - // If it exceeds the limit set in the config, reject the connection with a 429. - if(connections.length + 1 > this.Config.http.maxConnections) { - socket.write("HTTP/1.1 429 Too Many Requests\n\n429 Too Many Requests"); - socket.destroy(); - } - - this.socket.handleUpgrade(req, socket, head, (ws: WebSocket) => { - this.socket.emit('connection', ws, req); - this.onConnection(ws, req, ip); - }); - } - - private onConnection(ws : WebSocket, req: http.IncomingMessage, ip : string) { - - var _ipdata = this.ips.filter(data => data.address == ip); - var ipdata; - if(_ipdata.length > 0) { - ipdata = _ipdata[0]; - }else{ - - ipdata = new IPData(ip); - this.ips.push(ipdata); - } - - var user = new User(ws, ipdata, this.Config); - this.clients.push(user); - ws.on('error', (e) => { - - log("ERROR", `${e} (caused by connection ${ip})`); - ws.close(); - }); - ws.on('close', () => this.connectionClosed(user)); - ws.on('message', (e) => { - var msg; - try {msg = e.toString()} - catch { - // Close the user's connection if they send a non-string message - user.closeConnection(); - return; - } - this.onMessage(user, msg); - }); - if (this.Config.auth.enabled) { - user.sendMsg(guacutils.encode("auth", this.Config.auth.apiEndpoint)); - } - user.sendMsg(this.getAdduserMsg()); - log("INFO", `Connect from ${user.IP.address}`); - }; - - private connectionClosed(user : User) { - if (this.clients.indexOf(user) === -1) return; - if(user.IP.vote != null) { - user.IP.vote = null; - this.sendVoteUpdate(); - }; - if (this.indefiniteTurn === user) this.indefiniteTurn = null; - this.clients.splice(this.clients.indexOf(user), 1); - log("INFO", `Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ""}`); - if (!user.username) return; - if (this.TurnQueue.toArray().indexOf(user) !== -1) { - var hadturn = (this.TurnQueue.peek() === user); - this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter(u => u !== user)); - if (hadturn) this.nextTurn(); - } - - this.clients.forEach((c) => c.sendMsg(guacutils.encode("remuser", "1", user.username!))); - } - private async onMessage(client : User, message : string) { - var msgArr = guacutils.decode(message); - if (msgArr.length < 1) return; - switch (msgArr[0]) { - case "login": - if (msgArr.length !== 2 || !this.Config.auth.enabled) return; - if (!client.connectedToNode) { - client.sendMsg(guacutils.encode("login", "0", "You must connect to the VM before logging in.")); - return; - } - var res = await this.auth!.Authenticate(msgArr[1], client); - if (res.clientSuccess) { - log("INFO", `${client.IP.address} logged in as ${res.username}`); - client.sendMsg(guacutils.encode("login", "1")); - var old = this.clients.find(c=>c.username === res.username); - if (old) { - // kick() doesnt wait until the user is actually removed from the list and itd be anal to make it do that - // so we call connectionClosed manually here. When it gets called on kick(), it will return because the user isn't in the list - this.connectionClosed(old); - await old.kick(); - } - // Set username - this.renameUser(client, res.username); - // Set rank - client.rank = res.rank; - if (client.rank === Rank.Admin) { - client.sendMsg(guacutils.encode("admin", "0", "1")); - } else if (client.rank === Rank.Moderator) { - client.sendMsg(guacutils.encode("admin", "0", "3", this.ModPerms.toString())); - } - this.clients.forEach((c) => c.sendMsg(guacutils.encode("adduser", "1", client.username!, client.rank.toString()))); - } else { - client.sendMsg(guacutils.encode("login", "0", res.error!)); - if (res.error === "You are banned") { - client.kick(); - } - } - break; - case "list": - client.sendMsg(guacutils.encode("list", this.Config.collabvm.node, this.Config.collabvm.displayname, this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail())); - break; - case "connect": - if (!client.username || msgArr.length !== 2 || msgArr[1] !== this.Config.collabvm.node) { - client.sendMsg(guacutils.encode("connect", "0")); - return; - } - client.connectedToNode = true; - client.sendMsg(guacutils.encode("connect", "1", "1", this.Config.vm.snapshots ? "1" : "0", "0")); - if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg()); - if (this.Config.collabvm.motd) client.sendMsg(guacutils.encode("chat", "", this.Config.collabvm.motd)); - if (this.screenHidden) { - client.sendMsg(guacutils.encode("size", "0", "1024", "768")); - client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", this.screenHiddenImg)); - } else { - client.sendMsg(guacutils.encode("size", "0", this.VM.framebuffer.width.toString(), this.VM.framebuffer.height.toString())); - var jpg = this.VM.framebuffer.toBuffer("image/jpeg"); - var jpg64 = jpg.toString("base64"); - client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", jpg64)); - } - client.sendMsg(guacutils.encode("sync", Date.now().toString())); - if (this.voteInProgress) this.sendVoteUpdate(client); - this.sendTurnUpdate(client); - break; - case "view": - if(client.connectedToNode) return; - if(client.username || msgArr.length !== 3 || msgArr[1] !== this.Config.collabvm.node) { - // The use of connect here is intentional. - client.sendMsg(guacutils.encode("connect", "0")); - return; - } - - switch(msgArr[2]) { - case "0": - client.viewMode = 0; - break; - case "1": - client.viewMode = 1; - break; - default: - client.sendMsg(guacutils.encode("connect", "0")); - return; - } - - client.sendMsg(guacutils.encode("connect", "1", "1", this.Config.vm.snapshots ? "1" : "0", "0")); - if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg()); - if (this.Config.collabvm.motd) client.sendMsg(guacutils.encode("chat", "", this.Config.collabvm.motd)); - - if(client.viewMode == 1) { - if (this.screenHidden) { - client.sendMsg(guacutils.encode("size", "0", "1024", "768")); - client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", this.screenHiddenImg)); - } else { - client.sendMsg(guacutils.encode("size", "0", this.VM.framebuffer.width.toString(), this.VM.framebuffer.height.toString())); - var jpg = this.VM.framebuffer.toBuffer("image/jpeg"); - var jpg64 = jpg.toString("base64"); - client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", jpg64)); - } - client.sendMsg(guacutils.encode("sync", Date.now().toString())); - } - - if (this.voteInProgress) this.sendVoteUpdate(client); - this.sendTurnUpdate(client); - break; - case "rename": - if (!client.RenameRateLimit.request()) return; - if (client.connectedToNode && client.IP.muted) return; - if (this.Config.auth.enabled && client.rank !== Rank.Unregistered) { - client.sendMsg(guacutils.encode("chat", "", "Go to your account settings to change your username.")); - return; - } - if (this.Config.auth.enabled && msgArr[1] !== undefined) { - // Don't send system message to a user without a username since it was likely an automated attempt by the webapp - if (client.username) client.sendMsg(guacutils.encode("chat", "", "You need to log in to do that.")); - if (client.rank !== Rank.Unregistered) return; - this.renameUser(client, undefined); - return; - } - this.renameUser(client, msgArr[1]); - break; - case "chat": - if (!client.username) return; - if (client.IP.muted) return; - if (msgArr.length !== 2) return; - if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.chat) { - client.sendMsg(guacutils.encode("chat", "", "You need to login to do that.")); - return; - } - var msg = Utilities.HTMLSanitize(msgArr[1]); - // One of the things I hated most about the old server is it completely discarded your message if it was too long - if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength); - if (msg.trim().length < 1) return; - - this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", client.username!, msg))); - this.ChatHistory.push({user: client.username, msg: msg}); - client.onMsgSent(); - break; - case "turn": - if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && client.rank !== Rank.Admin && client.rank !== Rank.Moderator && client.rank !== Rank.Turn) return; - if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.turn) { - client.sendMsg(guacutils.encode("chat", "", "You need to login to do that.")); - return; - } - if (!client.TurnRateLimit.request()) return; - if (!client.connectedToNode) return; - if (msgArr.length > 2) return; - var takingTurn : boolean; - if (msgArr.length === 1) takingTurn = true; - else switch (msgArr[1]) { - case "0": - if (this.indefiniteTurn === client) { - this.indefiniteTurn = null; - } - takingTurn = false; - break; - case "1": - takingTurn = true; - break; - default: - return; - break; - } - if (takingTurn) { - var currentQueue = this.TurnQueue.toArray(); - // If the user is already in the turn queue, ignore the turn request. - if (currentQueue.indexOf(client) !== -1) return; - // If they're muted, also ignore the turn request. - // Send them the turn queue to prevent client glitches - if (client.IP.muted) return; - if(this.Config.collabvm.turnlimit.enabled) { - // Get the amount of users in the turn queue with the same IP as the user requesting a turn. - let turns = currentQueue.filter(user => user.IP.address == client.IP.address); - // If it exceeds the limit set in the config, ignore the turn request. - if(turns.length + 1 > this.Config.collabvm.turnlimit.maximum) return; - } - this.TurnQueue.enqueue(client); - if (this.TurnQueue.size === 1) this.nextTurn(); - } else { - var hadturn = (this.TurnQueue.peek() === client); - this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter(u => u !== client)); - if (hadturn) this.nextTurn(); - } - this.sendTurnUpdate(); - break; - case "mouse": - if (this.TurnQueue.peek() !== client && client.rank !== Rank.Admin) return; - if (!this.VM.acceptingInput()) return; - var x = parseInt(msgArr[1]); - var y = parseInt(msgArr[2]); - var mask = parseInt(msgArr[3]); - if (x === undefined || y === undefined || mask === undefined) return; - this.VM.pointerEvent(x, y, mask); - break; - case "key": - if (this.TurnQueue.peek() !== client && client.rank !== Rank.Admin) return; - if (!this.VM.acceptingInput()) return; - var keysym = parseInt(msgArr[1]); - var down = parseInt(msgArr[2]); - if (keysym === undefined || (down !== 0 && down !== 1)) return; - this.VM.keyEvent(keysym, down === 1 ? true : false); - break; - case "vote": - if (!this.Config.vm.snapshots) return; - if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && client.rank !== Rank.Admin && client.rank !== Rank.Moderator && client.rank !== Rank.Turn) return; - if (!client.connectedToNode) return; - if (msgArr.length !== 2) return; - if (!client.VoteRateLimit.request()) return; - switch (msgArr[1]) { - case "1": - if (!this.voteInProgress) { - if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.callForReset) { - client.sendMsg(guacutils.encode("chat", "", "You need to login to do that.")); - return; - } - if (this.voteCooldown !== 0) { - client.sendMsg(guacutils.encode("vote", "3", this.voteCooldown.toString())); - return; - } - this.startVote(); - this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", `${client.username} has started a vote to reset the VM.`))); - } - if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.vote) { - client.sendMsg(guacutils.encode("chat", "", "You need to login to do that.")); - return; - } - else if (client.IP.vote !== true) - this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", `${client.username} has voted yes.`))); - client.IP.vote = true; - break; - case "0": - if (!this.voteInProgress) return; - if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.vote) { - client.sendMsg(guacutils.encode("chat", "", "You need to login to do that.")); - return; - } - if (client.IP.vote !== false) - this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", `${client.username} has voted no.`))); - client.IP.vote = false; - break; - } - this.sendVoteUpdate(); - break; - case "admin": - if (msgArr.length < 2) return; - switch (msgArr[1]) { - case "2": - // Login - if (this.Config.auth.enabled) { - client.sendMsg(guacutils.encode("chat", "", "This server does not support staff passwords. Please log in to become staff.")); - return; - } - if (!client.LoginRateLimit.request() || !client.username) return; - if (msgArr.length !== 3) return; - var sha256 = createHash("sha256"); - sha256.update(msgArr[2]); - var pwdHash = sha256.digest('hex'); - sha256.destroy(); - if (pwdHash === this.Config.collabvm.adminpass) { - client.rank = Rank.Admin; - client.sendMsg(guacutils.encode("admin", "0", "1")); - } else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) { - client.rank = Rank.Moderator; - client.sendMsg(guacutils.encode("admin", "0", "3", this.ModPerms.toString())); - } else if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) { - client.rank = Rank.Turn; - client.sendMsg(guacutils.encode("chat", "", "You may now take turns.")); - } else { - client.sendMsg(guacutils.encode("admin", "0", "0")); - return; - } - if (this.screenHidden) { - client.sendMsg(guacutils.encode("size", "0", this.VM.framebuffer.width.toString(), this.VM.framebuffer.height.toString())); - var jpg = this.VM.framebuffer.toBuffer("image/jpeg"); - var jpg64 = jpg.toString("base64"); - client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", jpg64)); - client.sendMsg(guacutils.encode("sync", Date.now().toString())); - } - - this.clients.forEach((c) => c.sendMsg(guacutils.encode("adduser", "1", client.username!, client.rank.toString()))); - break; - case "5": - // QEMU Monitor - if (client.rank !== Rank.Admin) return; - if (!(this.VM instanceof QEMUVM)) { - client.sendMsg(guacutils.encode("admin", "2", "This is not a QEMU VM and therefore QEMU monitor commands cannot be run.")); - return; - } - if (msgArr.length !== 4 || msgArr[2] !== this.Config.collabvm.node) return; - var output = await this.VM.qmpClient.runMonitorCmd(msgArr[3]); - client.sendMsg(guacutils.encode("admin", "2", String(output))); - break; - case "8": - // Restore - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.restore)) return; - this.VM.Restore(); - break; - case "10": - // Reboot - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.reboot)) return; - if (msgArr.length !== 3 || msgArr[2] !== this.Config.collabvm.node) return; - this.VM.Reboot(); - break; - case "12": - // Ban - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.ban)) return; - var user = this.clients.find(c => c.username === msgArr[2]); - if (!user) return; - user.ban(); - case "13": - // Force Vote - if (msgArr.length !== 3) return; - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.forcevote)) return; - if (!this.voteInProgress) return; - switch (msgArr[2]) { - case "1": - this.endVote(true); - break; - case "0": - this.endVote(false); - break; - } - break; - case "14": - // Mute - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.mute)) return; - if (msgArr.length !== 4) return; - var user = this.clients.find(c => c.username === msgArr[2]); - if (!user) return; - var permamute; - switch (msgArr[3]) { - case "0": - permamute = false; - break; - case "1": - permamute = true; - break; - default: - return; - } - user.mute(permamute); - break; - case "15": - // Kick - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.kick)) return; - var user = this.clients.find(c => c.username === msgArr[2]); - if (!user) return; - user.kick(); - break; - case "16": - // End turn - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return; - if (msgArr.length !== 3) return; - var user = this.clients.find(c => c.username === msgArr[2]); - if (!user) return; - this.endTurn(user); - break; - case "17": - // Clear turn queue - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return; - if (msgArr.length !== 3 || msgArr[2] !== this.Config.collabvm.node) return; - this.clearTurns(); - break; - case "18": - // Rename user - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return; - if (this.Config.auth.enabled) { - client.sendMsg(guacutils.encode("chat", "", "Cannot rename users on a server that uses authentication.")); - } - if (msgArr.length !== 4) return; - var user = this.clients.find(c => c.username === msgArr[2]); - if (!user) return; - this.renameUser(user, msgArr[3]); - break; - case "19": - // Get IP - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.grabip)) return; - if (msgArr.length !== 3) return; - var user = this.clients.find(c => c.username === msgArr[2]); - if (!user) return; - client.sendMsg(guacutils.encode("admin", "19", msgArr[2], user.IP.address)); - break; - case "20": - // Steal turn - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return; - this.bypassTurn(client); - break; - case "21": - // XSS - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.xss)) return; - if (msgArr.length !== 3) return; - switch (client.rank) { - case Rank.Admin: - - this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", client.username!, msgArr[2]))); - - this.ChatHistory.push({user: client.username!, msg: msgArr[2]}); - break; - case Rank.Moderator: - - this.clients.filter(c => c.rank !== Rank.Admin).forEach(c => c.sendMsg(guacutils.encode("chat", client.username!, msgArr[2]))); - - this.clients.filter(c => c.rank === Rank.Admin).forEach(c => c.sendMsg(guacutils.encode("chat", client.username!, Utilities.HTMLSanitize(msgArr[2])))); - break; - } - break; - case "22": - // Toggle turns - if (client.rank !== Rank.Admin) return; - if (msgArr.length !== 3) return; - switch (msgArr[2]) { - case "0": - this.clearTurns(); - this.turnsAllowed = false; - break; - case "1": - this.turnsAllowed = true; - break; - } - break; - case "23": - // Indefinite turn - if (client.rank !== Rank.Admin) return; - this.indefiniteTurn = client; - this.TurnQueue = Queue.from([client, ...this.TurnQueue.toArray().filter(c=>c!==client)]); - this.sendTurnUpdate(); - break; - case "24": - // Hide screen - if (client.rank !== Rank.Admin) return; - if (msgArr.length !== 3) return; - switch (msgArr[2]) { - case "0": - this.screenHidden = true; - this.clients.filter(c => c.rank == Rank.Unregistered).forEach(client => { - client.sendMsg(guacutils.encode("size", "0", "1024", "768")); - client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", this.screenHiddenImg)); - client.sendMsg(guacutils.encode("sync", Date.now().toString())); - }); - break; - case "1": - this.screenHidden = false; - this.clients.forEach(client => { - client.sendMsg(guacutils.encode("size", "0", this.VM.framebuffer.width.toString(), this.VM.framebuffer.height.toString())); - var jpg = this.VM.framebuffer.toBuffer("image/jpeg"); - var jpg64 = jpg.toString("base64"); - client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", jpg64)); - client.sendMsg(guacutils.encode("sync", Date.now().toString())); - }); - break; - } - break; - case "25": - if (client.rank !== Rank.Admin || msgArr.length !== 3) - return; - this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", msgArr[2]))); - break; - - } - break; - - } - } - - getUsernameList() : string[] { - var arr : string[] = []; - - this.clients.filter(c => c.username).forEach((c) => arr.push(c.username!)); - return arr; - } - - renameUser(client : User, newName? : string) { - // This shouldn't need a ternary but it does for some reason - var hadName : boolean = client.username ? true : false; - var oldname : any; - if (hadName) oldname = client.username; - var status = "0"; - if (!newName) { - client.assignGuestName(this.getUsernameList()); - } else { - newName = newName.trim(); - if (hadName && newName === oldname) { - - client.sendMsg(guacutils.encode("rename", "0", "0", client.username!, client.rank.toString())); - return; - } - if (this.getUsernameList().indexOf(newName) !== -1) { - client.assignGuestName(this.getUsernameList()); - if(client.connectedToNode) { - status = "1"; - } - } else - if (!/^[a-zA-Z0-9\ \-\_\.]+$/.test(newName) || newName.length > 20 || newName.length < 3) { - client.assignGuestName(this.getUsernameList()); - status = "2"; - } else - if (this.Config.collabvm.usernameblacklist.indexOf(newName) !== -1) { - client.assignGuestName(this.getUsernameList()); - status = "3"; - } else client.username = newName; - } - - client.sendMsg(guacutils.encode("rename", "0", status, client.username!, client.rank.toString())); - if (hadName) { - log("INFO", `Rename ${client.IP.address} from ${oldname} to ${client.username}`); - this.clients.forEach((c) => - - c.sendMsg(guacutils.encode("rename", "1", oldname, client.username!, client.rank.toString()))); - } else { - log("INFO", `Rename ${client.IP.address} to ${client.username}`); - this.clients.forEach((c) => - - c.sendMsg(guacutils.encode("adduser", "1", client.username!, client.rank.toString()))); - } - } - - getAdduserMsg() : string { - var arr : string[] = ["adduser", this.clients.filter(c=>c.username).length.toString()]; - - this.clients.filter(c=>c.username).forEach((c) => arr.push(c.username!, c.rank.toString())); - return guacutils.encode(...arr); - } - getChatHistoryMsg() : string { - var arr : string[] = ["chat"]; - this.ChatHistory.forEach(c => arr.push(c.user, c.msg)); - return guacutils.encode(...arr); - } - private sendTurnUpdate(client? : User) { - var turnQueueArr = this.TurnQueue.toArray(); - var turntime; - if (this.indefiniteTurn === null) turntime = (this.TurnTime * 1000); - else turntime = 9999999999; - var arr = ["turn", turntime.toString(), this.TurnQueue.size.toString()]; - // @ts-ignore - this.TurnQueue.forEach((c) => arr.push(c.username)); - var currentTurningUser = this.TurnQueue.peek(); - if (client) { - client.sendMsg(guacutils.encode(...arr)); - return; - } - this.clients.filter(c => (c !== currentTurningUser && c.connectedToNode)).forEach((c) => { - if (turnQueueArr.indexOf(c) !== -1) { - var time; - if (this.indefiniteTurn === null) time = ((this.TurnTime * 1000) + ((turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000)); - else time = 9999999999; - c.sendMsg(guacutils.encode(...arr, time.toString())); - } else { - c.sendMsg(guacutils.encode(...arr)); - } - }); - if (currentTurningUser) - currentTurningUser.sendMsg(guacutils.encode(...arr)); - } - private nextTurn() { - clearInterval(this.TurnInterval); - if (this.TurnQueue.size === 0) { - } else { - this.TurnTime = this.Config.collabvm.turnTime; - this.TurnInterval = setInterval(() => this.turnInterval(), 1000); - } - this.sendTurnUpdate(); - } - - clearTurns() { - clearInterval(this.TurnInterval); - this.TurnQueue.clear(); - this.sendTurnUpdate(); - } - - bypassTurn(client : User) { - var a = this.TurnQueue.toArray().filter(c => c !== client); - this.TurnQueue = Queue.from([client, ...a]); - this.nextTurn(); - } - - endTurn(client : User) { - var hasTurn = (this.TurnQueue.peek() === client); - this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter(c => c !== client)); - if (hasTurn) this.nextTurn(); - else this.sendTurnUpdate(); - } - - private turnInterval() { - if (this.indefiniteTurn !== null) return; - this.TurnTime--; - if (this.TurnTime < 1) { - this.TurnQueue.dequeue(); - this.nextTurn(); - } - } - - private async newrect(rect : Canvas, x : number, y : number) { - var jpg = rect.toBuffer("image/jpeg", {quality: 0.5, progressive: true, chromaSubsampling: true}); - var jpg64 = jpg.toString("base64"); - this.clients.filter(c => c.connectedToNode || c.viewMode == 1).forEach(c => { - if (this.screenHidden && c.rank == Rank.Unregistered) return; - c.sendMsg(guacutils.encode("png", "0", "0", x.toString(), y.toString(), jpg64)); - c.sendMsg(guacutils.encode("sync", Date.now().toString())); - }); - } - - private newsize(size : {height:number,width:number}) { - this.clients.filter(c => c.connectedToNode || c.viewMode == 1).forEach(c => { - if (this.screenHidden && c.rank == Rank.Unregistered) return; - c.sendMsg(guacutils.encode("size", "0", size.width.toString(), size.height.toString())) - }); - } - - getThumbnail() : Promise { - return new Promise(async (res, rej) => { - var cnv = createCanvas(400, 300); - var ctx = cnv.getContext("2d"); - ctx.drawImage(this.VM.framebuffer, 0, 0, 400, 300); - var jpg = cnv.toBuffer("image/jpeg"); - res(jpg.toString("base64")); - }) - } - - startVote() { - if (this.voteInProgress) return; - this.voteInProgress = true; - this.clients.forEach(c => c.sendMsg(guacutils.encode("vote", "0"))); - this.voteTime = this.Config.collabvm.voteTime; - this.voteInterval = setInterval(() => { - this.voteTime--; - if (this.voteTime < 1) { - this.endVote(); - } - }, 1000); - } - - endVote(result? : boolean) { - if (!this.voteInProgress) return; - this.voteInProgress = false; - clearInterval(this.voteInterval); - var count = this.getVoteCounts(); - this.clients.forEach((c) => c.sendMsg(guacutils.encode("vote", "2"))); - if (result === true || (result === undefined && count.yes >= count.no)) { - this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", "The vote to reset the VM has won."))); - this.VM.Restore(); - } else { - this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", "The vote to reset the VM has lost."))); - } - this.clients.forEach(c => { - c.IP.vote = null; - }); - this.voteCooldown = this.Config.collabvm.voteCooldown; - this.voteCooldownInterval = setInterval(() => { - this.voteCooldown--; - if (this.voteCooldown < 1) - clearInterval(this.voteCooldownInterval); - }, 1000); - } - - sendVoteUpdate(client? : User) { - if (!this.voteInProgress) return; - var count = this.getVoteCounts(); - var msg = guacutils.encode("vote", "1", (this.voteTime * 1000).toString(), count.yes.toString(), count.no.toString()); - if (client) - client.sendMsg(msg); - else - this.clients.forEach((c) => c.sendMsg(msg)); - } - - getVoteCounts() : {yes:number,no:number} { - var yes = 0; - var no = 0; - this.ips.forEach((c) => { - if (c.vote === true) yes++; - if (c.vote === false) no++; - }); - return {yes:yes,no:no}; - } -} diff --git a/src/guacutils.ts b/src/guacutils.ts deleted file mode 100644 index 0e0d41c..0000000 --- a/src/guacutils.ts +++ /dev/null @@ -1,43 +0,0 @@ -export function decode(string : string) : string[] { - let pos = -1; - let sections = []; - - for(;;) { - let len = string.indexOf('.', pos + 1); - - if(len === -1) - break; - - pos = parseInt(string.slice(pos + 1, len)) + len + 1; - - // don't allow funky protocol length - if(pos > string.length) - return []; - - sections.push(string.slice(len + 1, pos)); - - - const sep = string.slice(pos, pos + 1); - - if(sep === ',') - continue; - else if(sep === ';') - break; - else - // Invalid data. - return []; - } - - return sections; -} - -export function encode(...string : string[]) : string { - let command = ''; - - for(var i = 0; i < string.length; i++) { - let current = string[i]; - command += current.toString().length + '.' + current; - command += ( i < string.length - 1 ? ',' : ';'); - } - return command; -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index b2d0f71..0000000 --- a/src/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as toml from 'toml'; -import IConfig from './IConfig.js'; -import * as fs from "fs"; -import WSServer from './WSServer.js'; -import QEMUVM from './QEMUVM.js'; -import log from './log.js'; -import AuthManager from './AuthManager.js'; - -log("INFO", "CollabVM Server starting up"); - -// Parse the config file - -var Config : IConfig; - -if (!fs.existsSync("config.toml")) { - log("FATAL", "Config.toml not found. Please copy config.example.toml and fill out fields") - process.exit(1); -} -try { - var configRaw = fs.readFileSync("config.toml").toString(); - Config = toml.parse(configRaw); -} catch (e) { - log("FATAL", `Failed to read or parse the config file: ${e}`); - process.exit(1); -} - - -async function start() { - // Print a warning if qmpSockDir is set - // and the host OS is Windows, as this - // configuration will very likely not work. - if(process.platform === "win32" && Config.vm.qmpSockDir) { - log("WARN", "You appear to have the option 'qmpSockDir' enabled in the config.") - log("WARN", "This is not supported on Windows, and you will likely run into issues."); - log("WARN", "To remove this warning, use the qmpHost and qmpPort options instead."); - } - - // Init the auth manager if enabled - var auth = Config.auth.enabled ? new AuthManager(Config.auth.apiEndpoint, Config.auth.secretKey) : null; - - // Fire up the VM - var VM = new QEMUVM(Config); - await VM.Start(); - - // Start up the websocket server - var WS = new WSServer(Config, VM, auth); - WS.listen(); -} -start(); \ No newline at end of file diff --git a/src/log.ts b/src/log.ts deleted file mode 100644 index ee1928c..0000000 --- a/src/log.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default function log(loglevel : string, ...message : string[]) { - console[ - (loglevel === "ERROR" || loglevel === "FATAL") ? "error" : - (loglevel === "WARN") ? "warn" : - "log" - ](`[${new Date().toLocaleString()}] [${loglevel}]`, ...message); -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index af23762..1579da8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,103 +1,11 @@ +// This is the base tsconfig the entire cvmts project uses { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "ES2022", /* Specify what module code is generated. */ - "rootDir": "./src", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [".js", ".d.ts", ".ts", ".mjs", ".cjs", ".json"], /* List of file name suffixes to search when resolving a module. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./build", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Node", + "types": ["node"], + "allowSyntheticDefaultImports": true, + "strict": true, + } } diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..087e547 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4265 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@babel/code-frame@npm:^7.0.0": + version: 7.24.2 + resolution: "@babel/code-frame@npm:7.24.2" + dependencies: + "@babel/highlight": "npm:^7.24.2" + picocolors: "npm:^1.0.0" + checksum: 10c0/d1d4cba89475ab6aab7a88242e1fd73b15ecb9f30c109b69752956434d10a26a52cbd37727c4eca104b6d45227bd1dfce39a6a6f4a14c9b2f07f871e968cf406 + languageName: node + linkType: hard + +"@babel/helper-validator-identifier@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-validator-identifier@npm:7.22.20" + checksum: 10c0/dcad63db345fb110e032de46c3688384b0008a42a4845180ce7cd62b1a9c0507a1bed727c4d1060ed1a03ae57b4d918570259f81724aaac1a5b776056f37504e + languageName: node + linkType: hard + +"@babel/highlight@npm:^7.24.2": + version: 7.24.2 + resolution: "@babel/highlight@npm:7.24.2" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.22.20" + chalk: "npm:^2.4.2" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.0.0" + checksum: 10c0/98ce00321daedeed33a4ed9362dc089a70375ff1b3b91228b9f05e6591d387a81a8cba68886e207861b8871efa0bc997ceabdd9c90f6cce3ee1b2f7f941b42db + languageName: node + linkType: hard + +"@computernewb/nodejs-rfb@npm:^0.3.0": + version: 0.3.0 + resolution: "@computernewb/nodejs-rfb@npm:0.3.0" + checksum: 10c0/915a76118011a4a64e28fcc91fafcffe659e5e86db2505554578f1e5dff4883dbaf5fcc013ac377719fe9c798e730b178aa5030027da0f5a9583355ec2ac8af2 + languageName: node + linkType: hard + +"@computernewb/superqemu@npm:^0.1.0": + version: 0.1.0 + resolution: "@computernewb/superqemu@npm:0.1.0" + dependencies: + "@computernewb/nodejs-rfb": "npm:^0.3.0" + execa: "npm:^8.0.1" + pino: "npm:^9.3.1" + checksum: 10c0/7177b46c1093345cc3cbcc09450b8b8b09f09eb74ba5abd283aae39e9d1dbc0780f54187da075e44c78a7b683d47367010a473406c2817c36352edd0ddad2c1a + languageName: node + linkType: hard + +"@cvmts/collab-vm-1.2-binary-protocol@workspace:collab-vm-1.2-binary-protocol": + version: 0.0.0-use.local + resolution: "@cvmts/collab-vm-1.2-binary-protocol@workspace:collab-vm-1.2-binary-protocol" + dependencies: + "@parcel/packager-ts": "npm:2.12.0" + "@parcel/transformer-typescript-types": "npm:2.12.0" + parcel: "npm:^2.12.0" + typescript: "npm:>=3.0.0" + languageName: unknown + linkType: soft + +"@cvmts/cvm-rs@npm:*, @cvmts/cvm-rs@workspace:cvm-rs": + version: 0.0.0-use.local + resolution: "@cvmts/cvm-rs@workspace:cvm-rs" + dependencies: + cargo-cp-artifact: "npm:^0.1" + languageName: unknown + linkType: soft + +"@cvmts/cvmts@workspace:cvmts": + version: 0.0.0-use.local + resolution: "@cvmts/cvmts@workspace:cvmts" + dependencies: + "@computernewb/nodejs-rfb": "npm:^0.3.0" + "@computernewb/superqemu": "npm:^0.1.0" + "@cvmts/cvm-rs": "npm:*" + "@maxmind/geoip2-node": "npm:^5.0.0" + "@types/node": "npm:^20.12.5" + "@types/ws": "npm:^8.5.5" + execa: "npm:^8.0.1" + mnemonist: "npm:^0.39.5" + msgpackr: "npm:^1.10.2" + pino: "npm:^9.3.1" + pino-pretty: "npm:^11.2.1" + prettier: "npm:^3.2.5" + sharp: "npm:^0.33.3" + toml: "npm:^3.0.0" + typescript: "npm:^5.4.4" + ws: "npm:^8.14.1" + languageName: unknown + linkType: soft + +"@emnapi/runtime@npm:^1.1.0": + version: 1.1.1 + resolution: "@emnapi/runtime@npm:1.1.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/c11ee57abf0ec643e64ccdace4b4fcc0b0c7b1117a191f969e84ae3669841aa90d2c17fa35b73f5a66fc0c843c8caca7bf11187faaeaa526bcfb7dbfb9b85de9 + languageName: node + linkType: hard + +"@img/sharp-darwin-arm64@npm:0.33.3": + version: 0.33.3 + resolution: "@img/sharp-darwin-arm64@npm:0.33.3" + dependencies: + "@img/sharp-libvips-darwin-arm64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-darwin-arm64": + optional: true + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-darwin-x64@npm:0.33.3": + version: 0.33.3 + resolution: "@img/sharp-darwin-x64@npm:0.33.3" + dependencies: + "@img/sharp-libvips-darwin-x64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-darwin-x64": + optional: true + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-darwin-arm64@npm:1.0.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-x64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-darwin-x64@npm:1.0.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linux-arm64@npm:1.0.2" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linux-arm@npm:1.0.2" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-s390x@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linux-s390x@npm:1.0.2" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-x64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linux-x64@npm:1.0.2" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.0.2" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-x64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.0.2" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linux-arm64@npm:0.33.3": + version: 0.33.3 + resolution: "@img/sharp-linux-arm64@npm:0.33.3" + dependencies: + "@img/sharp-libvips-linux-arm64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linux-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-arm@npm:0.33.3": + version: 0.33.3 + resolution: "@img/sharp-linux-arm@npm:0.33.3" + dependencies: + "@img/sharp-libvips-linux-arm": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linux-arm": + optional: true + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-s390x@npm:0.33.3": + version: 0.33.3 + resolution: "@img/sharp-linux-s390x@npm:0.33.3" + dependencies: + "@img/sharp-libvips-linux-s390x": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linux-s390x": + optional: true + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-x64@npm:0.33.3": + version: 0.33.3 + resolution: "@img/sharp-linux-x64@npm:0.33.3" + dependencies: + "@img/sharp-libvips-linux-x64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linux-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-arm64@npm:0.33.3": + version: 0.33.3 + resolution: "@img/sharp-linuxmusl-arm64@npm:0.33.3" + dependencies: + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-x64@npm:0.33.3": + version: 0.33.3 + resolution: "@img/sharp-linuxmusl-x64@npm:0.33.3" + dependencies: + "@img/sharp-libvips-linuxmusl-x64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-wasm32@npm:0.33.3": + version: 0.33.3 + resolution: "@img/sharp-wasm32@npm:0.33.3" + dependencies: + "@emnapi/runtime": "npm:^1.1.0" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@img/sharp-win32-ia32@npm:0.33.3": + version: 0.33.3 + resolution: "@img/sharp-win32-ia32@npm:0.33.3" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@img/sharp-win32-x64@npm:0.33.3": + version: 0.33.3 + resolution: "@img/sharp-win32-x64@npm:0.33.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 10c0/b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + languageName: node + linkType: hard + +"@lezer/common@npm:^1.0.0": + version: 1.2.1 + resolution: "@lezer/common@npm:1.2.1" + checksum: 10c0/af61436dc026f8deebaded13d8e1beea2ae307cbbfb270116cdedadb8208f0674da9c3b5963128a2b1cd4072b4e90bc8128133f4feaf31b6e801e4568f1a15a6 + languageName: node + linkType: hard + +"@lezer/lr@npm:^1.0.0": + version: 1.4.0 + resolution: "@lezer/lr@npm:1.4.0" + dependencies: + "@lezer/common": "npm:^1.0.0" + checksum: 10c0/1e3af297cc142bb6676cb3c4e1bd310da2e93b53273cf745dc85d839a08e1d3cbbd67e0fc0322a480cf25ee215fefe967c53bc2af3ddf5d9b1bf267081dfa164 + languageName: node + linkType: hard + +"@lmdb/lmdb-darwin-arm64@npm:2.8.5": + version: 2.8.5 + resolution: "@lmdb/lmdb-darwin-arm64@npm:2.8.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@lmdb/lmdb-darwin-x64@npm:2.8.5": + version: 2.8.5 + resolution: "@lmdb/lmdb-darwin-x64@npm:2.8.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@lmdb/lmdb-linux-arm64@npm:2.8.5": + version: 2.8.5 + resolution: "@lmdb/lmdb-linux-arm64@npm:2.8.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@lmdb/lmdb-linux-arm@npm:2.8.5": + version: 2.8.5 + resolution: "@lmdb/lmdb-linux-arm@npm:2.8.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@lmdb/lmdb-linux-x64@npm:2.8.5": + version: 2.8.5 + resolution: "@lmdb/lmdb-linux-x64@npm:2.8.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@lmdb/lmdb-win32-x64@npm:2.8.5": + version: 2.8.5 + resolution: "@lmdb/lmdb-win32-x64@npm:2.8.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@maxmind/geoip2-node@npm:^5.0.0": + version: 5.0.0 + resolution: "@maxmind/geoip2-node@npm:5.0.0" + dependencies: + ip6addr: "npm:^0.2.5" + maxmind: "npm:^4.2.0" + checksum: 10c0/10f6c936b45632210210750b839578c610a3ceba06aff5db2a3d9da68b51b986caa7e700c78ab2ea02524b3793e4f21daee7ecfde1dc242241291e43833b7087 + languageName: node + linkType: hard + +"@mischnic/json-sourcemap@npm:^0.1.0": + version: 0.1.1 + resolution: "@mischnic/json-sourcemap@npm:0.1.1" + dependencies: + "@lezer/common": "npm:^1.0.0" + "@lezer/lr": "npm:^1.0.0" + json5: "npm:^2.2.1" + checksum: 10c0/e2e314fc048a16baedb10ec4d517c2622e464b8a9f8481cd4c008ebdabed1e5167a8f1407e06a14bb89f035addbb13851c1c5b6672ef8e089205f7f6d300cdd8 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.2": + version: 3.0.2 + resolution: "@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.2": + version: 3.0.2 + resolution: "@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.2": + version: 3.0.2 + resolution: "@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.2" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.2": + version: 3.0.2 + resolution: "@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.2" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.2": + version: 3.0.2 + resolution: "@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.2" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.2": + version: 3.0.2 + resolution: "@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^2.0.0": + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10c0/325e0db7b287d4154ecd164c0815c08007abfb07653cc57bceded17bb7fd240998a3cbdbe87d700e30bef494885eccc725ab73b668020811d56623d145b524ae + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.0 + resolution: "@npmcli/fs@npm:3.1.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/162b4a0b8705cd6f5c2470b851d1dc6cd228c86d2170e1769d738c1fbb69a87160901411c3c035331e9e99db72f1f1099a8b734bf1637cc32b9a5be1660e4e1e + languageName: node + linkType: hard + +"@parcel/bundler-default@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/bundler-default@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/graph": "npm:3.2.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/rust": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + nullthrows: "npm:^1.1.1" + checksum: 10c0/797e7494c82f2669a8d8d409b2efa2c956d2ac4edd5cd1b85560bbd7696483edb8ec220f66cdd88f7a3e47cfb346f33b21818c96f5a2bac098d5eef5085475d8 + languageName: node + linkType: hard + +"@parcel/cache@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/cache@npm:2.12.0" + dependencies: + "@parcel/fs": "npm:2.12.0" + "@parcel/logger": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + lmdb: "npm:2.8.5" + peerDependencies: + "@parcel/core": ^2.12.0 + checksum: 10c0/ef80c88a754d2e1c9161eb8e518f4a4b03c186001384100d037e333a1c00b4a701b0f6c1743a1663c6bb7e20d09c8582584f44ebea0fc6d81c81b4a81a1d0b6b + languageName: node + linkType: hard + +"@parcel/codeframe@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/codeframe@npm:2.12.0" + dependencies: + chalk: "npm:^4.1.0" + checksum: 10c0/23a73d8a5b6a7612ab6a5918ad52631f58d3529758730517a0ce151f0c533e5b4b1788278dd521d4863dd0e0b972afb590af69cb8523b14e809279825da549a1 + languageName: node + linkType: hard + +"@parcel/compressor-raw@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/compressor-raw@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + checksum: 10c0/e057b38d3cae862048f3777ea97544e60465e8efc16ecab0b8602d9c2787c80a09ac3bb338f773af5c17a6b4356caf103986951b47022fdf02b21c5e0b600033 + languageName: node + linkType: hard + +"@parcel/config-default@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/config-default@npm:2.12.0" + dependencies: + "@parcel/bundler-default": "npm:2.12.0" + "@parcel/compressor-raw": "npm:2.12.0" + "@parcel/namer-default": "npm:2.12.0" + "@parcel/optimizer-css": "npm:2.12.0" + "@parcel/optimizer-htmlnano": "npm:2.12.0" + "@parcel/optimizer-image": "npm:2.12.0" + "@parcel/optimizer-svgo": "npm:2.12.0" + "@parcel/optimizer-swc": "npm:2.12.0" + "@parcel/packager-css": "npm:2.12.0" + "@parcel/packager-html": "npm:2.12.0" + "@parcel/packager-js": "npm:2.12.0" + "@parcel/packager-raw": "npm:2.12.0" + "@parcel/packager-svg": "npm:2.12.0" + "@parcel/packager-wasm": "npm:2.12.0" + "@parcel/reporter-dev-server": "npm:2.12.0" + "@parcel/resolver-default": "npm:2.12.0" + "@parcel/runtime-browser-hmr": "npm:2.12.0" + "@parcel/runtime-js": "npm:2.12.0" + "@parcel/runtime-react-refresh": "npm:2.12.0" + "@parcel/runtime-service-worker": "npm:2.12.0" + "@parcel/transformer-babel": "npm:2.12.0" + "@parcel/transformer-css": "npm:2.12.0" + "@parcel/transformer-html": "npm:2.12.0" + "@parcel/transformer-image": "npm:2.12.0" + "@parcel/transformer-js": "npm:2.12.0" + "@parcel/transformer-json": "npm:2.12.0" + "@parcel/transformer-postcss": "npm:2.12.0" + "@parcel/transformer-posthtml": "npm:2.12.0" + "@parcel/transformer-raw": "npm:2.12.0" + "@parcel/transformer-react-refresh-wrap": "npm:2.12.0" + "@parcel/transformer-svg": "npm:2.12.0" + peerDependencies: + "@parcel/core": ^2.12.0 + checksum: 10c0/c3fec515c14479f1a0041db79a70198f04bea94a03a7f331257f057de178d4e0061b68853c2e83d45f891d09fadb8b4361f38421832b6e116edd46f8e0ee51a9 + languageName: node + linkType: hard + +"@parcel/core@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/core@npm:2.12.0" + dependencies: + "@mischnic/json-sourcemap": "npm:^0.1.0" + "@parcel/cache": "npm:2.12.0" + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/events": "npm:2.12.0" + "@parcel/fs": "npm:2.12.0" + "@parcel/graph": "npm:3.2.0" + "@parcel/logger": "npm:2.12.0" + "@parcel/package-manager": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/profiler": "npm:2.12.0" + "@parcel/rust": "npm:2.12.0" + "@parcel/source-map": "npm:^2.1.1" + "@parcel/types": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + "@parcel/workers": "npm:2.12.0" + abortcontroller-polyfill: "npm:^1.1.9" + base-x: "npm:^3.0.8" + browserslist: "npm:^4.6.6" + clone: "npm:^2.1.1" + dotenv: "npm:^7.0.0" + dotenv-expand: "npm:^5.1.0" + json5: "npm:^2.2.0" + msgpackr: "npm:^1.9.9" + nullthrows: "npm:^1.1.1" + semver: "npm:^7.5.2" + checksum: 10c0/ab6b4bc1e95b0aaee23c5aec8479cf6681cf84a0c422e1001a3a0f3957aa28756851eb201a89d8b55ce84912c8987a76597f77193ade771f034c1c33a07ece44 + languageName: node + linkType: hard + +"@parcel/diagnostic@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/diagnostic@npm:2.12.0" + dependencies: + "@mischnic/json-sourcemap": "npm:^0.1.0" + nullthrows: "npm:^1.1.1" + checksum: 10c0/61c2fce32a1abdf343a4d2e3a109779dc2a9c255059e4dd70ad9b4b3bd5b11b676d0c42bc77e4b32e886ef471be018b25b952baa9da137c066410642d2d0507f + languageName: node + linkType: hard + +"@parcel/events@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/events@npm:2.12.0" + checksum: 10c0/0f0a0b02086b81d68cf8f239414e9e09b5a6eca6dddfd22d2e922979b2d85b03e6f68bcafa2c6434c46867c908e25f2002f47f0ed5551f2674a75f4d6c5731ff + languageName: node + linkType: hard + +"@parcel/fs@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/fs@npm:2.12.0" + dependencies: + "@parcel/rust": "npm:2.12.0" + "@parcel/types": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + "@parcel/watcher": "npm:^2.0.7" + "@parcel/workers": "npm:2.12.0" + peerDependencies: + "@parcel/core": ^2.12.0 + checksum: 10c0/5d9ebf62e80dc3781fcd1eb763da46188115e254d285690383539a085aeaf9d864a54655046223ea42815b9b308ecba80d9af53cff6390c6bbb37d2b29df8e35 + languageName: node + linkType: hard + +"@parcel/graph@npm:3.2.0": + version: 3.2.0 + resolution: "@parcel/graph@npm:3.2.0" + dependencies: + nullthrows: "npm:^1.1.1" + checksum: 10c0/acb98a9c44dbabaa38e2a7b6b07aa489d75dc207ed6107ea43575d3c68ebf388a65a982d85677c7d00cd2d7bb6f8a6f75df9618a53389e9f640aa9346fb75c3b + languageName: node + linkType: hard + +"@parcel/logger@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/logger@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/events": "npm:2.12.0" + checksum: 10c0/b33782bbf0cfff30169a4ee8dd3a1d14c9b2c0d4781715e26b5dc6f2321ddff8ca84eca8de40bccb1a8c5d3ce847494408f5db63bbeddcdaaf9b82b1cc376a17 + languageName: node + linkType: hard + +"@parcel/markdown-ansi@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/markdown-ansi@npm:2.12.0" + dependencies: + chalk: "npm:^4.1.0" + checksum: 10c0/0c203c70ab1eb12f4976c32b086b2abf5dc841b42310610e70e1e713fe915acfd0942b56a78456811a9ee150226bb44052910a3f98ea56289aafa36b6ce89e27 + languageName: node + linkType: hard + +"@parcel/namer-default@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/namer-default@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + nullthrows: "npm:^1.1.1" + checksum: 10c0/5baffe07af2329315b9d2b897565b915038246afaa3269d81bcd5eb4bcc7a21771bf1171918d68a67c099584b006167beeefa4716fb4557aae4bc112ebaf4159 + languageName: node + linkType: hard + +"@parcel/node-resolver-core@npm:3.3.0": + version: 3.3.0 + resolution: "@parcel/node-resolver-core@npm:3.3.0" + dependencies: + "@mischnic/json-sourcemap": "npm:^0.1.0" + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/fs": "npm:2.12.0" + "@parcel/rust": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + nullthrows: "npm:^1.1.1" + semver: "npm:^7.5.2" + checksum: 10c0/9a2731763514c0a54da9710e1131b5960b928900cbc33faf67d07a892cf9ed9f1b11ed2653e574e8363c4527d16e008365917b7b09eb3b9ee727fd244a5f51ee + languageName: node + linkType: hard + +"@parcel/optimizer-css@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/optimizer-css@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/source-map": "npm:^2.1.1" + "@parcel/utils": "npm:2.12.0" + browserslist: "npm:^4.6.6" + lightningcss: "npm:^1.22.1" + nullthrows: "npm:^1.1.1" + checksum: 10c0/537e84a85fda7a2f73acd2a55842ffe9846abb02d18a7518baf8ae140fc6140a26bb1988285dbccb49a883fdc8597eabbb6d4882500bf160b97d6d93e3664677 + languageName: node + linkType: hard + +"@parcel/optimizer-htmlnano@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/optimizer-htmlnano@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + htmlnano: "npm:^2.0.0" + nullthrows: "npm:^1.1.1" + posthtml: "npm:^0.16.5" + svgo: "npm:^2.4.0" + checksum: 10c0/487e0fa99e975e6f9add2759e4ad412c0595d7b80d5dde9e186700fa54a9ecb9d1cb611fbd5a0d3392fda3a01050d95e3ded53ca8b50ede3203fe77af489cd0b + languageName: node + linkType: hard + +"@parcel/optimizer-image@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/optimizer-image@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/rust": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + "@parcel/workers": "npm:2.12.0" + peerDependencies: + "@parcel/core": ^2.12.0 + checksum: 10c0/f050c569548ec8330c65d0e9b6f6b15d5761e14e704ef16b950db19ae0d6b5a30fd42a38bb04841561244e8ab8f7fb781d9e9f1418ae84858fe7ad325a4be494 + languageName: node + linkType: hard + +"@parcel/optimizer-svgo@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/optimizer-svgo@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + svgo: "npm:^2.4.0" + checksum: 10c0/dc49c565d8f15b4f78ee70910a9c527f25316f0440e9cba6c5b8af1562d34708e5276b35f1e1ea26e7911d6d5c60fa82be6627517fe818df6f69eba5f0f6813f + languageName: node + linkType: hard + +"@parcel/optimizer-swc@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/optimizer-swc@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/source-map": "npm:^2.1.1" + "@parcel/utils": "npm:2.12.0" + "@swc/core": "npm:^1.3.36" + nullthrows: "npm:^1.1.1" + checksum: 10c0/52f52182769ebb76248deab85893dacf183e6ff9a87a56c3589331cb0e37debb7ae8fa819386fe23f69b15e6b39823879e20816b10fbab3d316018a94b0c653c + languageName: node + linkType: hard + +"@parcel/package-manager@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/package-manager@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/fs": "npm:2.12.0" + "@parcel/logger": "npm:2.12.0" + "@parcel/node-resolver-core": "npm:3.3.0" + "@parcel/types": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + "@parcel/workers": "npm:2.12.0" + "@swc/core": "npm:^1.3.36" + semver: "npm:^7.5.2" + peerDependencies: + "@parcel/core": ^2.12.0 + checksum: 10c0/3ebffe05b293332f69c34479ea0b51a9fa3449ab56eef1b0ec9487c4feacf45df6dec9d8dcb67203398249093370f7d884dc0cb6b6ee15ee8c5db9768579060c + languageName: node + linkType: hard + +"@parcel/packager-css@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/packager-css@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/source-map": "npm:^2.1.1" + "@parcel/utils": "npm:2.12.0" + lightningcss: "npm:^1.22.1" + nullthrows: "npm:^1.1.1" + checksum: 10c0/a7293c84c67b9e07b8b8cdc48d96037e05bc50daa8a2aba64b23797fea87e259bf7046a5b969917531db33b8f2387463c817e569a34f42d791bbfacb074268ea + languageName: node + linkType: hard + +"@parcel/packager-html@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/packager-html@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + "@parcel/types": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + nullthrows: "npm:^1.1.1" + posthtml: "npm:^0.16.5" + checksum: 10c0/099eccde796af61cb6f153fcd69c49d22b4acc430d3652a4f2e5d4124c1cf2d6782213048436fd8e9e5521a52b1219e7bc02d38be89ce97e6f70899d3be31d7f + languageName: node + linkType: hard + +"@parcel/packager-js@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/packager-js@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/rust": "npm:2.12.0" + "@parcel/source-map": "npm:^2.1.1" + "@parcel/types": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + globals: "npm:^13.2.0" + nullthrows: "npm:^1.1.1" + checksum: 10c0/89214e8d35f6dc35c2fd0c2b11ec608703dbc52435a7a6141e0b8fc676610fa09c2210cc93490ea4b3581ae0bc13f307dd5515402c939980e1c6bf90148d34e2 + languageName: node + linkType: hard + +"@parcel/packager-raw@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/packager-raw@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + checksum: 10c0/c1539179a62674460fea65c9fd1b150aedd596723e79d4e949bf5bd667defd6a72ed73552033e4cdd2b854aa6d5022201797b746e5deb633b41f1de716716af9 + languageName: node + linkType: hard + +"@parcel/packager-svg@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/packager-svg@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + "@parcel/types": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + posthtml: "npm:^0.16.4" + checksum: 10c0/58f877d470e5b50adb7eca837f571cbd221cf6681bc83d08146e4aeae4e1430a2e3363beb4a62cfc6952f4f8ded1746889545b4c946300258268a11b298047fd + languageName: node + linkType: hard + +"@parcel/packager-ts@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/packager-ts@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + checksum: 10c0/244f94c0d33cfb76e429196bf826e73f508ee4e4af7aa736b02ff3de65ace8ea12a0d7bf8d7d1e6c2ee4d0d5f1db8564db01d58379b08cf8bd4c90d0476f053b + languageName: node + linkType: hard + +"@parcel/packager-wasm@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/packager-wasm@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + checksum: 10c0/bd3ccd6f9a0506b26b0d708ded6cea3ac53df5c49426086b556ba7f9f1351aba010da3e0795a1f6944cdc306cffc08eed249bb8444aa4f44d9de0e3d1592810d + languageName: node + linkType: hard + +"@parcel/plugin@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/plugin@npm:2.12.0" + dependencies: + "@parcel/types": "npm:2.12.0" + checksum: 10c0/2030a3e1ee6b8cdfdf07935b085f7731e286651d7455b84a7f635016c580af715deffb893c5bc9fb3e0126db4511d3f2b592ee17b61108d001339d51ef56f9bf + languageName: node + linkType: hard + +"@parcel/profiler@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/profiler@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/events": "npm:2.12.0" + chrome-trace-event: "npm:^1.0.2" + checksum: 10c0/3caa9014da88f7435c43396fd1bb413c35134801699943717079a92fcd3ab0a0974c98b98473c5bc1ef434ce8203483fb96af642c1d64e20266625499ca4b4fe + languageName: node + linkType: hard + +"@parcel/reporter-cli@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/reporter-cli@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + "@parcel/types": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + chalk: "npm:^4.1.0" + term-size: "npm:^2.2.1" + checksum: 10c0/0fee616377d540e11e61fd827a8886d8b8fc4985f87da694945b5a7f3da821bcbb0c5d7a31d72cdf12546c7bf555f7ef5c15d75b71ab157d93cacf0972b29006 + languageName: node + linkType: hard + +"@parcel/reporter-dev-server@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/reporter-dev-server@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + checksum: 10c0/bd875c937214aa877805413dbfce89d95dc2578098693991cce26624366cc19807a678c2779edbc620f9618db244807a2271027fb5e328318618a4666b33e512 + languageName: node + linkType: hard + +"@parcel/reporter-tracer@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/reporter-tracer@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + chrome-trace-event: "npm:^1.0.3" + nullthrows: "npm:^1.1.1" + checksum: 10c0/5ab33196ce4a62681d5017d908da354e25a6d367cdf0a849cd408c673bac61d3674316438a4c4c7eebb26f865e5ee3c1b8cda897c92dfa7211c0107c48d04388 + languageName: node + linkType: hard + +"@parcel/resolver-default@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/resolver-default@npm:2.12.0" + dependencies: + "@parcel/node-resolver-core": "npm:3.3.0" + "@parcel/plugin": "npm:2.12.0" + checksum: 10c0/22b1e4223070c962570928390c6cb77e866d4a3ede1a7019ad3ed2fed75604a2d78c335d65aa646dd753f05916397b56416aef52009cace9b56fd39bf6362457 + languageName: node + linkType: hard + +"@parcel/runtime-browser-hmr@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/runtime-browser-hmr@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + checksum: 10c0/126babc8dbd7937e94a38bed1527190a203c20bcba7b66f85b1ddbce81ec54b3fb0579f371284cb7290b70fc46b88eaaa1ee6c9d9e3b739b6267d6902dc82f93 + languageName: node + linkType: hard + +"@parcel/runtime-js@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/runtime-js@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + nullthrows: "npm:^1.1.1" + checksum: 10c0/01cb236c0ab6f6a170ead43d519ba02092d9b1805f2b8e8cce6f0fec4cb2c37e885c8ce0ff8ae4c7025499d1e36d1ff755f5e8018172c4245c01e97c7a3e9a21 + languageName: node + linkType: hard + +"@parcel/runtime-react-refresh@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/runtime-react-refresh@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + react-error-overlay: "npm:6.0.9" + react-refresh: "npm:^0.9.0" + checksum: 10c0/9efd3903118169f1eb4c176afbc4b8ee38d8b516a72dd189fec4d05c5b216e105aa6a77dd87aa5966923a648ed2c227e83feaed6c706a6fd5ebe0cdf255d5d46 + languageName: node + linkType: hard + +"@parcel/runtime-service-worker@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/runtime-service-worker@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + nullthrows: "npm:^1.1.1" + checksum: 10c0/014e44aa15bcc81002713af1cfc88a1d010f3ba6565ec5ea560231540a79cb76fecb11336ac019fb4c9c21a59477a1ce2d9f1a67f85e07be6b7da4498cfa17b3 + languageName: node + linkType: hard + +"@parcel/rust@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/rust@npm:2.12.0" + checksum: 10c0/38d8e5c69b31b3f7eb431f479c250f7a4e37f7814ce0aa16d42c300fffa25659da0ea8ca8e22534fa2b935dc8559507829d0cdebb588756aa4c3619565dcd3e3 + languageName: node + linkType: hard + +"@parcel/source-map@npm:^2.1.1": + version: 2.1.1 + resolution: "@parcel/source-map@npm:2.1.1" + dependencies: + detect-libc: "npm:^1.0.3" + checksum: 10c0/cea8450e152666be413556f0d100f125e81646bffc497e7c792bd9fc5067d052f1a008c8404ce1cd3a587d58b9ef57207ada89149cf2c705e71b1978308045f6 + languageName: node + linkType: hard + +"@parcel/transformer-babel@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/transformer-babel@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/source-map": "npm:^2.1.1" + "@parcel/utils": "npm:2.12.0" + browserslist: "npm:^4.6.6" + json5: "npm:^2.2.0" + nullthrows: "npm:^1.1.1" + semver: "npm:^7.5.2" + checksum: 10c0/b7398cc2ef02fd76010bb522fc72e562ce835643365a37ccfc56368121e5c9d890bef14fffa40a8c69e4a26f13ee7d6da8d8e8590957bd4f363b5aa1c4f7d43d + languageName: node + linkType: hard + +"@parcel/transformer-css@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/transformer-css@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/source-map": "npm:^2.1.1" + "@parcel/utils": "npm:2.12.0" + browserslist: "npm:^4.6.6" + lightningcss: "npm:^1.22.1" + nullthrows: "npm:^1.1.1" + checksum: 10c0/b3ad2591bca09a5696791b9a50bfb8efb825e92313740d6e3988ae1345d70965e92f9d42d58ae5571749e422d9018681aa49bddeafa939f3948a6993cc1cb4c8 + languageName: node + linkType: hard + +"@parcel/transformer-html@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/transformer-html@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/rust": "npm:2.12.0" + nullthrows: "npm:^1.1.1" + posthtml: "npm:^0.16.5" + posthtml-parser: "npm:^0.10.1" + posthtml-render: "npm:^3.0.0" + semver: "npm:^7.5.2" + srcset: "npm:4" + checksum: 10c0/1e73c1afe87b8db36e358752fe1b89d466cd9bfe66dda34fca58ad28ab10931553b16ba82096eeb266a0d90e62d6c9e455e3b32dbdf550f4212193898d4c45fd + languageName: node + linkType: hard + +"@parcel/transformer-image@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/transformer-image@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + "@parcel/workers": "npm:2.12.0" + nullthrows: "npm:^1.1.1" + peerDependencies: + "@parcel/core": ^2.12.0 + checksum: 10c0/e361fa97d81b3dc2dfe011342321f1d2afd4fd41a9c2791522d8f39e2dc94714a2a0b9d291eb73437b2023fd1493ad37046d6b1ee925ec80daa18261cd5767a4 + languageName: node + linkType: hard + +"@parcel/transformer-js@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/transformer-js@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/rust": "npm:2.12.0" + "@parcel/source-map": "npm:^2.1.1" + "@parcel/utils": "npm:2.12.0" + "@parcel/workers": "npm:2.12.0" + "@swc/helpers": "npm:^0.5.0" + browserslist: "npm:^4.6.6" + nullthrows: "npm:^1.1.1" + regenerator-runtime: "npm:^0.13.7" + semver: "npm:^7.5.2" + peerDependencies: + "@parcel/core": ^2.12.0 + checksum: 10c0/8a438f0ae93539338ac3f2e2666377e75fb8a5a5386c84485d6cf5f0ad5e52862a80da89c35ca01fae10184ccc7567f1347679fd3b514f7b86643dc83dbce6a6 + languageName: node + linkType: hard + +"@parcel/transformer-json@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/transformer-json@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + json5: "npm:^2.2.0" + checksum: 10c0/41f931eacf89b5a792ca906594eeafa75d9fe5d0af85af7cf42e77f04e1d31de5bd64d3da9fcf0bdf745f3af252dd727ac318b12cc1c3a1345d19c5096ad98d8 + languageName: node + linkType: hard + +"@parcel/transformer-postcss@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/transformer-postcss@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/rust": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + clone: "npm:^2.1.1" + nullthrows: "npm:^1.1.1" + postcss-value-parser: "npm:^4.2.0" + semver: "npm:^7.5.2" + checksum: 10c0/24c3a7eedd741ec1df43bed64b7e02e0132e1c85b9a93322fc994fd2a7f457c4a45f624edf3c064630f947749eb1eb89cb5a502db3f6a39286880afe09020e5a + languageName: node + linkType: hard + +"@parcel/transformer-posthtml@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/transformer-posthtml@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + nullthrows: "npm:^1.1.1" + posthtml: "npm:^0.16.5" + posthtml-parser: "npm:^0.10.1" + posthtml-render: "npm:^3.0.0" + semver: "npm:^7.5.2" + checksum: 10c0/ae626c15d5dda547850511a8aed41ba35e9496861dbba24efcb904693ced003a74f25c454b0f4bb96500725dd7e09ed4d09becccc48c0c8cdf8fde3ba02aa3f0 + languageName: node + linkType: hard + +"@parcel/transformer-raw@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/transformer-raw@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + checksum: 10c0/3a23729c6f91ef22c106995f730483dd375f81c11f8bb37ff485d6f3c111f64445d437796d470b42bdd2ee75cc3c4a142911fbcddd1676c8659dfc5e886917d2 + languageName: node + linkType: hard + +"@parcel/transformer-react-refresh-wrap@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/transformer-react-refresh-wrap@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + react-refresh: "npm:^0.9.0" + checksum: 10c0/37dd835182bf71fcee5858f0ab16d5683d2827b4930095ed9fffbd496e431a7f1c53de598f294220b7ff27cd5264d5f1fa750d974a1ee02fb39342fd867b6f9c + languageName: node + linkType: hard + +"@parcel/transformer-sass@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/transformer-sass@npm:2.12.0" + dependencies: + "@parcel/plugin": "npm:2.12.0" + "@parcel/source-map": "npm:^2.1.1" + sass: "npm:^1.38.0" + checksum: 10c0/d82ee90605f9a8f04a978e2d72faf9693a843a0875961b6e7aa40659197356f1be45d738840c52863607c7823f70630f819aebd843606af8ea00772b75ba7574 + languageName: node + linkType: hard + +"@parcel/transformer-svg@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/transformer-svg@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/rust": "npm:2.12.0" + nullthrows: "npm:^1.1.1" + posthtml: "npm:^0.16.5" + posthtml-parser: "npm:^0.10.1" + posthtml-render: "npm:^3.0.0" + semver: "npm:^7.5.2" + checksum: 10c0/8916bdc0b36c60b32963e015c43a8bcd8cc2b15cc11b7611c49af6a4e4d63c2aabea0aa0fde31a78278eec25f88b52b3e56d8382dc2db5f3a401e63312115f3a + languageName: node + linkType: hard + +"@parcel/transformer-typescript-types@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/transformer-typescript-types@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/plugin": "npm:2.12.0" + "@parcel/source-map": "npm:^2.1.1" + "@parcel/ts-utils": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + nullthrows: "npm:^1.1.1" + peerDependencies: + typescript: ">=3.0.0" + checksum: 10c0/6b0e89b64f2cfc5e56b972b02a6a1b97c50e60b6ab935389ebf9479d05d164fe4ff9dc297b0d78ed3d3578f80049425b056512546d45877f549a6713d54efd03 + languageName: node + linkType: hard + +"@parcel/ts-utils@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/ts-utils@npm:2.12.0" + dependencies: + nullthrows: "npm:^1.1.1" + peerDependencies: + typescript: ">=3.0.0" + checksum: 10c0/be53613cb3d950f5a6f0690af4bdda1ec9aaf0e97a035c66271230bd322ae29568d10831e61ded899188ca36d80add8ccbfcb494ebd4b0d9795cb51ccb7425cc + languageName: node + linkType: hard + +"@parcel/types@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/types@npm:2.12.0" + dependencies: + "@parcel/cache": "npm:2.12.0" + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/fs": "npm:2.12.0" + "@parcel/package-manager": "npm:2.12.0" + "@parcel/source-map": "npm:^2.1.1" + "@parcel/workers": "npm:2.12.0" + utility-types: "npm:^3.10.0" + checksum: 10c0/a8aa61ad7cc8218a41fe27c206031b30c55eab59cd4affdfac7d15ddcfb80a1969c22086760b7d4fbdd63016dbfe3278d462e04b9c12e474780fe154caf08150 + languageName: node + linkType: hard + +"@parcel/utils@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/utils@npm:2.12.0" + dependencies: + "@parcel/codeframe": "npm:2.12.0" + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/logger": "npm:2.12.0" + "@parcel/markdown-ansi": "npm:2.12.0" + "@parcel/rust": "npm:2.12.0" + "@parcel/source-map": "npm:^2.1.1" + chalk: "npm:^4.1.0" + nullthrows: "npm:^1.1.1" + checksum: 10c0/888e2352d056ceff4e81d0cf4ae4eb8f322b0a8c4eb9e6f6aa5f916adc3f27c90369d5580b4f316029bf5160294a607795181a6bb368741524c177a14b2aa7c7 + languageName: node + linkType: hard + +"@parcel/watcher-android-arm64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-android-arm64@npm:2.4.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-arm64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-darwin-arm64@npm:2.4.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-x64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-darwin-x64@npm:2.4.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-freebsd-x64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-freebsd-x64@npm:2.4.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm-glibc@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-arm-glibc@npm:2.4.1" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-glibc@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-arm64-glibc@npm:2.4.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-musl@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-arm64-musl@npm:2.4.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-glibc@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-x64-glibc@npm:2.4.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-musl@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-x64-musl@npm:2.4.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-win32-arm64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-win32-arm64@npm:2.4.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-win32-ia32@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-win32-ia32@npm:2.4.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@parcel/watcher-win32-x64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-win32-x64@npm:2.4.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher@npm:^2.0.7": + version: 2.4.1 + resolution: "@parcel/watcher@npm:2.4.1" + dependencies: + "@parcel/watcher-android-arm64": "npm:2.4.1" + "@parcel/watcher-darwin-arm64": "npm:2.4.1" + "@parcel/watcher-darwin-x64": "npm:2.4.1" + "@parcel/watcher-freebsd-x64": "npm:2.4.1" + "@parcel/watcher-linux-arm-glibc": "npm:2.4.1" + "@parcel/watcher-linux-arm64-glibc": "npm:2.4.1" + "@parcel/watcher-linux-arm64-musl": "npm:2.4.1" + "@parcel/watcher-linux-x64-glibc": "npm:2.4.1" + "@parcel/watcher-linux-x64-musl": "npm:2.4.1" + "@parcel/watcher-win32-arm64": "npm:2.4.1" + "@parcel/watcher-win32-ia32": "npm:2.4.1" + "@parcel/watcher-win32-x64": "npm:2.4.1" + detect-libc: "npm:^1.0.3" + is-glob: "npm:^4.0.3" + micromatch: "npm:^4.0.5" + node-addon-api: "npm:^7.0.0" + node-gyp: "npm:latest" + dependenciesMeta: + "@parcel/watcher-android-arm64": + optional: true + "@parcel/watcher-darwin-arm64": + optional: true + "@parcel/watcher-darwin-x64": + optional: true + "@parcel/watcher-freebsd-x64": + optional: true + "@parcel/watcher-linux-arm-glibc": + optional: true + "@parcel/watcher-linux-arm64-glibc": + optional: true + "@parcel/watcher-linux-arm64-musl": + optional: true + "@parcel/watcher-linux-x64-glibc": + optional: true + "@parcel/watcher-linux-x64-musl": + optional: true + "@parcel/watcher-win32-arm64": + optional: true + "@parcel/watcher-win32-ia32": + optional: true + "@parcel/watcher-win32-x64": + optional: true + checksum: 10c0/33b7112094b9eb46c234d824953967435b628d3d93a0553255e9910829b84cab3da870153c3a870c31db186dc58f3b2db81382fcaee3451438aeec4d786a6211 + languageName: node + linkType: hard + +"@parcel/workers@npm:2.12.0": + version: 2.12.0 + resolution: "@parcel/workers@npm:2.12.0" + dependencies: + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/logger": "npm:2.12.0" + "@parcel/profiler": "npm:2.12.0" + "@parcel/types": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + nullthrows: "npm:^1.1.1" + peerDependencies: + "@parcel/core": ^2.12.0 + checksum: 10c0/0f5e12e7997d806d6694e91a6c5968c34e1967f50bab3c09296589b2b279ffcd1c8de735845448de350e510a5657ba0aeb4b2c5c04cab81c4c7a57f70d567f5e + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + languageName: node + linkType: hard + +"@swc/core-darwin-arm64@npm:1.4.17": + version: 1.4.17 + resolution: "@swc/core-darwin-arm64@npm:1.4.17" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-darwin-x64@npm:1.4.17": + version: 1.4.17 + resolution: "@swc/core-darwin-x64@npm:1.4.17" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@swc/core-linux-arm-gnueabihf@npm:1.4.17": + version: 1.4.17 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.4.17" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@swc/core-linux-arm64-gnu@npm:1.4.17": + version: 1.4.17 + resolution: "@swc/core-linux-arm64-gnu@npm:1.4.17" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-arm64-musl@npm:1.4.17": + version: 1.4.17 + resolution: "@swc/core-linux-arm64-musl@npm:1.4.17" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-linux-x64-gnu@npm:1.4.17": + version: 1.4.17 + resolution: "@swc/core-linux-x64-gnu@npm:1.4.17" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-x64-musl@npm:1.4.17": + version: 1.4.17 + resolution: "@swc/core-linux-x64-musl@npm:1.4.17" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-win32-arm64-msvc@npm:1.4.17": + version: 1.4.17 + resolution: "@swc/core-win32-arm64-msvc@npm:1.4.17" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-win32-ia32-msvc@npm:1.4.17": + version: 1.4.17 + resolution: "@swc/core-win32-ia32-msvc@npm:1.4.17" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@swc/core-win32-x64-msvc@npm:1.4.17": + version: 1.4.17 + resolution: "@swc/core-win32-x64-msvc@npm:1.4.17" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@swc/core@npm:^1.3.36": + version: 1.4.17 + resolution: "@swc/core@npm:1.4.17" + dependencies: + "@swc/core-darwin-arm64": "npm:1.4.17" + "@swc/core-darwin-x64": "npm:1.4.17" + "@swc/core-linux-arm-gnueabihf": "npm:1.4.17" + "@swc/core-linux-arm64-gnu": "npm:1.4.17" + "@swc/core-linux-arm64-musl": "npm:1.4.17" + "@swc/core-linux-x64-gnu": "npm:1.4.17" + "@swc/core-linux-x64-musl": "npm:1.4.17" + "@swc/core-win32-arm64-msvc": "npm:1.4.17" + "@swc/core-win32-ia32-msvc": "npm:1.4.17" + "@swc/core-win32-x64-msvc": "npm:1.4.17" + "@swc/counter": "npm:^0.1.2" + "@swc/types": "npm:^0.1.5" + peerDependencies: + "@swc/helpers": ^0.5.0 + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10c0/385b1ced6ed3b282c717f422d7fb70a8529f81b004dacb6fd49b3cc3693f33047d806870fae868ea71b586628aaf6879870afacd495c61103fe4f46bda8a83e3 + languageName: node + linkType: hard + +"@swc/counter@npm:^0.1.2, @swc/counter@npm:^0.1.3": + version: 0.1.3 + resolution: "@swc/counter@npm:0.1.3" + checksum: 10c0/8424f60f6bf8694cfd2a9bca45845bce29f26105cda8cf19cdb9fd3e78dc6338699e4db77a89ae449260bafa1cc6bec307e81e7fb96dbf7dcfce0eea55151356 + languageName: node + linkType: hard + +"@swc/helpers@npm:^0.5.0": + version: 0.5.10 + resolution: "@swc/helpers@npm:0.5.10" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/db7d82cf1301d01a92777795abe6846fd0a0af15bf52c37f1f2945cdafd96ebc612276820fc4c04e0b875b7109b5f4087e796afe7ddba36c1137d895144db2e2 + languageName: node + linkType: hard + +"@swc/types@npm:^0.1.5": + version: 0.1.6 + resolution: "@swc/types@npm:0.1.6" + dependencies: + "@swc/counter": "npm:^0.1.3" + checksum: 10c0/043a0e56d69db8733827ad69db55d0ffbd6976fd24ef629a488e57040067ac84d057a57e08bc5a3db545d44b01d6aa43c22df1152c637af450d366e57cde6e22 + languageName: node + linkType: hard + +"@trysound/sax@npm:0.2.0": + version: 0.2.0 + resolution: "@trysound/sax@npm:0.2.0" + checksum: 10c0/44907308549ce775a41c38a815f747009ac45929a45d642b836aa6b0a536e4978d30b8d7d680bbd116e9dd73b7dbe2ef0d1369dcfc2d09e83ba381e485ecbe12 + languageName: node + linkType: hard + +"@types/node@npm:*, @types/node@npm:^20.12.5": + version: 20.12.7 + resolution: "@types/node@npm:20.12.7" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10c0/dce80d63a3b91892b321af823d624995c61e39c6a223cc0ac481a44d337640cc46931d33efb3beeed75f5c85c3bda1d97cef4c5cd4ec333caf5dee59cff6eca0 + languageName: node + linkType: hard + +"@types/node@npm:^20.14.10": + version: 20.14.10 + resolution: "@types/node@npm:20.14.10" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10c0/0b06cff14365c2d0085dc16cc8cbea5c40ec09cfc1fea966be9eeecf35562760bfde8f88e86de6edfaf394501236e229d9c1084fad04fb4dec472ae245d8ae69 + languageName: node + linkType: hard + +"@types/ws@npm:^8.5.5": + version: 8.5.10 + resolution: "@types/ws@npm:8.5.10" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/e9af279b984c4a04ab53295a40aa95c3e9685f04888df5c6920860d1dd073fcc57c7bd33578a04b285b2c655a0b52258d34bee0a20569dca8defb8393e1e5d29 + languageName: node + linkType: hard + +"@types/yauzl@npm:^2.9.1": + version: 2.10.3 + resolution: "@types/yauzl@npm:2.10.3" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/f1b7c1b99fef9f2fe7f1985ef7426d0cebe48cd031f1780fcdc7451eec7e31ac97028f16f50121a59bcf53086a1fc8c856fd5b7d3e00970e43d92ae27d6b43dc + languageName: node + linkType: hard + +"abbrev@npm:^2.0.0": + version: 2.0.0 + resolution: "abbrev@npm:2.0.0" + checksum: 10c0/f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 + languageName: node + linkType: hard + +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: "npm:^5.0.0" + checksum: 10c0/90ccc50f010250152509a344eb2e71977fbf8db0ab8f1061197e3275ddf6c61a41a6edfd7b9409c664513131dd96e962065415325ef23efa5db931b382d24ca5 + languageName: node + linkType: hard + +"abortcontroller-polyfill@npm:^1.1.9": + version: 1.7.5 + resolution: "abortcontroller-polyfill@npm:1.7.5" + checksum: 10c0/d7a5ab6fda4f9a54f22ddeb233a2564d2f4f857ec17be25fee21a91bb5090bee57c630c454634b5c4b93fc06bd90d592d1f2fc69f77cd28791ac0fe361feb7d2 + languageName: node + linkType: hard + +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": + version: 7.1.1 + resolution: "agent-base@npm:7.1.1" + dependencies: + debug: "npm:^4.3.4" + checksum: 10c0/e59ce7bed9c63bf071a30cc471f2933862044c97fd9958967bfe22521d7a0f601ce4ed5a8c011799d0c726ca70312142ae193bbebb60f576b52be19d4a363b50 + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: "npm:^2.0.0" + indent-string: "npm:^4.0.0" + checksum: 10c0/a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: 10c0/cbe16dbd2c6b2735d1df7976a7070dd277326434f0212f43abf6d87674095d247968209babdaad31bb00882fa68807256ba9be340eec2f1004de14ca75f52a08 + languageName: node + linkType: hard + +"ansi-styles@npm:^3.2.1": + version: 3.2.1 + resolution: "ansi-styles@npm:3.2.1" + dependencies: + color-convert: "npm:^1.9.0" + checksum: 10c0/ece5a8ef069fcc5298f67e3f4771a663129abd174ea2dfa87923a2be2abf6cd367ef72ac87942da00ce85bd1d651d4cd8595aebdb1b385889b89b205860e977b + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c + languageName: node + linkType: hard + +"anymatch@npm:~3.1.2": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac + languageName: node + linkType: hard + +"argparse@npm:^2.0.1": + version: 2.0.1 + resolution: "argparse@npm:2.0.1" + checksum: 10c0/c5640c2d89045371c7cedd6a70212a04e360fd34d6edeae32f6952c63949e3525ea77dbec0289d8213a99bbaeab5abfa860b5c12cf88a2e6cf8106e90dd27a7e + languageName: node + linkType: hard + +"assert-plus@npm:1.0.0, assert-plus@npm:^1.0.0": + version: 1.0.0 + resolution: "assert-plus@npm:1.0.0" + checksum: 10c0/b194b9d50c3a8f872ee85ab110784911e696a4d49f7ee6fc5fb63216dedbefd2c55999c70cb2eaeb4cf4a0e0338b44e9ace3627117b5bf0d42460e9132f21b91 + languageName: node + linkType: hard + +"atomic-sleep@npm:^1.0.0": + version: 1.0.0 + resolution: "atomic-sleep@npm:1.0.0" + checksum: 10c0/e329a6665512736a9bbb073e1761b4ec102f7926cce35037753146a9db9c8104f5044c1662e4a863576ce544fb8be27cd2be6bc8c1a40147d03f31eb1cfb6e8a + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + languageName: node + linkType: hard + +"base-x@npm:^3.0.8": + version: 3.0.9 + resolution: "base-x@npm:3.0.9" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10c0/e6bbeae30b24f748b546005affb710c5fbc8b11a83f6cd0ca999bd1ab7ad3a22e42888addc40cd145adc4edfe62fcfab4ebc91da22e4259aae441f95a77aee1a + languageName: node + linkType: hard + +"base64-js@npm:^1.3.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf + languageName: node + linkType: hard + +"binary-extensions@npm:^2.0.0": + version: 2.3.0 + resolution: "binary-extensions@npm:2.3.0" + checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5 + languageName: node + linkType: hard + +"boolbase@npm:^1.0.0": + version: 1.0.0 + resolution: "boolbase@npm:1.0.0" + checksum: 10c0/e4b53deb4f2b85c52be0e21a273f2045c7b6a6ea002b0e139c744cb6f95e9ec044439a52883b0d74dedd1ff3da55ed140cfdddfed7fb0cccbed373de5dce1bcf + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: "npm:^1.0.0" + checksum: 10c0/b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f + languageName: node + linkType: hard + +"braces@npm:^3.0.2, braces@npm:~3.0.2": + version: 3.0.2 + resolution: "braces@npm:3.0.2" + dependencies: + fill-range: "npm:^7.0.1" + checksum: 10c0/321b4d675791479293264019156ca322163f02dc06e3c4cab33bb15cd43d80b51efef69b0930cfde3acd63d126ebca24cd0544fa6f261e093a0fb41ab9dda381 + languageName: node + linkType: hard + +"browserslist@npm:^4.6.6": + version: 4.23.0 + resolution: "browserslist@npm:4.23.0" + dependencies: + caniuse-lite: "npm:^1.0.30001587" + electron-to-chromium: "npm:^1.4.668" + node-releases: "npm:^2.0.14" + update-browserslist-db: "npm:^1.0.13" + bin: + browserslist: cli.js + checksum: 10c0/8e9cc154529062128d02a7af4d8adeead83ca1df8cd9ee65a88e2161039f3d68a4d40fea7353cab6bae4c16182dec2fdd9a1cf7dc2a2935498cee1af0e998943 + languageName: node + linkType: hard + +"buffer-crc32@npm:~0.2.3": + version: 0.2.13 + resolution: "buffer-crc32@npm:0.2.13" + checksum: 10c0/cb0a8ddf5cf4f766466db63279e47761eb825693eeba6a5a95ee4ec8cb8f81ede70aa7f9d8aeec083e781d47154290eb5d4d26b3f7a465ec57fb9e7d59c47150 + languageName: node + linkType: hard + +"buffer@npm:^6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.2.1" + checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0 + languageName: node + linkType: hard + +"cacache@npm:^18.0.0": + version: 18.0.2 + resolution: "cacache@npm:18.0.2" + dependencies: + "@npmcli/fs": "npm:^3.1.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^10.0.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^4.0.0" + ssri: "npm:^10.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^3.0.0" + checksum: 10c0/7992665305cc251a984f4fdbab1449d50e88c635bc43bf2785530c61d239c61b349e5734461baa461caaee65f040ab14e2d58e694f479c0810cffd181ba5eabc + languageName: node + linkType: hard + +"callsites@npm:^3.0.0": + version: 3.1.0 + resolution: "callsites@npm:3.1.0" + checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301 + languageName: node + linkType: hard + +"caniuse-lite@npm:^1.0.30001587": + version: 1.0.30001612 + resolution: "caniuse-lite@npm:1.0.30001612" + checksum: 10c0/d6b405ff06f4e913bc779f9183fa68001c9d6b8526a7dd1b99c60587dd21a01aa8def3d8462cf6214f0181f1c21b9245611ff65241cf9c967fc742e86ece5065 + languageName: node + linkType: hard + +"cargo-cp-artifact@npm:^0.1": + version: 0.1.9 + resolution: "cargo-cp-artifact@npm:0.1.9" + bin: + cargo-cp-artifact: bin/cargo-cp-artifact.js + checksum: 10c0/60eb1845917cfb021920fcf600a72379890b385396f9c69107face3b16b347960b66cd3d82cc169c6ac8b1212cf0706584125bc36fbc08353b033310c17ca0a6 + languageName: node + linkType: hard + +"chalk@npm:^2.4.2": + version: 2.4.2 + resolution: "chalk@npm:2.4.2" + dependencies: + ansi-styles: "npm:^3.2.1" + escape-string-regexp: "npm:^1.0.5" + supports-color: "npm:^5.3.0" + checksum: 10c0/e6543f02ec877732e3a2d1c3c3323ddb4d39fbab687c23f526e25bd4c6a9bf3b83a696e8c769d078e04e5754921648f7821b2a2acfd16c550435fd630026e073 + languageName: node + linkType: hard + +"chalk@npm:^4.1.0": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: "npm:^4.1.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880 + languageName: node + linkType: hard + +"chokidar@npm:>=3.0.0 <4.0.0": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462 + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: 10c0/594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 + languageName: node + linkType: hard + +"chrome-trace-event@npm:^1.0.2, chrome-trace-event@npm:^1.0.3": + version: 1.0.3 + resolution: "chrome-trace-event@npm:1.0.3" + checksum: 10c0/080ce2d20c2b9e0f8461a380e9585686caa768b1c834a464470c9dc74cda07f27611c7b727a2cd768a9cecd033297fdec4ce01f1e58b62227882c1059dec321c + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 10c0/1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 + languageName: node + linkType: hard + +"clone@npm:^2.1.1": + version: 2.1.2 + resolution: "clone@npm:2.1.2" + checksum: 10c0/ed0601cd0b1606bc7d82ee7175b97e68d1dd9b91fd1250a3617b38d34a095f8ee0431d40a1a611122dcccb4f93295b4fdb94942aa763392b5fe44effa50c2d5e + languageName: node + linkType: hard + +"color-convert@npm:^1.9.0": + version: 1.9.3 + resolution: "color-convert@npm:1.9.3" + dependencies: + color-name: "npm:1.1.3" + checksum: 10c0/5ad3c534949a8c68fca8fbc6f09068f435f0ad290ab8b2f76841b9e6af7e0bb57b98cb05b0e19fe33f5d91e5a8611ad457e5f69e0a484caad1f7487fd0e8253c + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 + languageName: node + linkType: hard + +"color-name@npm:1.1.3": + version: 1.1.3 + resolution: "color-name@npm:1.1.3" + checksum: 10c0/566a3d42cca25b9b3cd5528cd7754b8e89c0eb646b7f214e8e2eaddb69994ac5f0557d9c175eb5d8f0ad73531140d9c47525085ee752a91a2ab15ab459caf6d6 + languageName: node + linkType: hard + +"color-name@npm:^1.0.0, color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 + languageName: node + linkType: hard + +"color-string@npm:^1.9.0": + version: 1.9.1 + resolution: "color-string@npm:1.9.1" + dependencies: + color-name: "npm:^1.0.0" + simple-swizzle: "npm:^0.2.2" + checksum: 10c0/b0bfd74c03b1f837f543898b512f5ea353f71630ccdd0d66f83028d1f0924a7d4272deb278b9aef376cacf1289b522ac3fb175e99895283645a2dc3a33af2404 + languageName: node + linkType: hard + +"color@npm:^4.2.3": + version: 4.2.3 + resolution: "color@npm:4.2.3" + dependencies: + color-convert: "npm:^2.0.1" + color-string: "npm:^1.9.0" + checksum: 10c0/7fbe7cfb811054c808349de19fb380252e5e34e61d7d168ec3353e9e9aacb1802674bddc657682e4e9730c2786592a4de6f8283e7e0d3870b829bb0b7b2f6118 + languageName: node + linkType: hard + +"colorette@npm:^2.0.7": + version: 2.0.20 + resolution: "colorette@npm:2.0.20" + checksum: 10c0/e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40 + languageName: node + linkType: hard + +"commander@npm:^7.0.0, commander@npm:^7.2.0": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 10c0/8d690ff13b0356df7e0ebbe6c59b4712f754f4b724d4f473d3cc5b3fdcf978e3a5dc3078717858a2ceb50b0f84d0660a7f22a96cdc50fb877d0c9bb31593d23a + languageName: node + linkType: hard + +"core-util-is@npm:1.0.2": + version: 1.0.2 + resolution: "core-util-is@npm:1.0.2" + checksum: 10c0/980a37a93956d0de8a828ce508f9b9e3317039d68922ca79995421944146700e4aaf490a6dbfebcb1c5292a7184600c7710b957d724be1e37b8254c6bc0fe246 + languageName: node + linkType: hard + +"cosmiconfig@npm:^8.0.0": + version: 8.3.6 + resolution: "cosmiconfig@npm:8.3.6" + dependencies: + import-fresh: "npm:^3.3.0" + js-yaml: "npm:^4.1.0" + parse-json: "npm:^5.2.0" + path-type: "npm:^4.0.0" + peerDependencies: + typescript: ">=4.9.5" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/0382a9ed13208f8bfc22ca2f62b364855207dffdb73dc26e150ade78c3093f1cf56172df2dd460c8caf2afa91c0ed4ec8a88c62f8f9cd1cf423d26506aa8797a + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10c0/5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + languageName: node + linkType: hard + +"css-select@npm:^4.1.3": + version: 4.3.0 + resolution: "css-select@npm:4.3.0" + dependencies: + boolbase: "npm:^1.0.0" + css-what: "npm:^6.0.1" + domhandler: "npm:^4.3.1" + domutils: "npm:^2.8.0" + nth-check: "npm:^2.0.1" + checksum: 10c0/a489d8e5628e61063d5a8fe0fa1cc7ae2478cb334a388a354e91cf2908154be97eac9fa7ed4dffe87a3e06cf6fcaa6016553115335c4fd3377e13dac7bd5a8e1 + languageName: node + linkType: hard + +"css-tree@npm:^1.1.2, css-tree@npm:^1.1.3": + version: 1.1.3 + resolution: "css-tree@npm:1.1.3" + dependencies: + mdn-data: "npm:2.0.14" + source-map: "npm:^0.6.1" + checksum: 10c0/499a507bfa39b8b2128f49736882c0dd636b0cd3370f2c69f4558ec86d269113286b7df469afc955de6a68b0dba00bc533e40022a73698081d600072d5d83c1c + languageName: node + linkType: hard + +"css-what@npm:^6.0.1": + version: 6.1.0 + resolution: "css-what@npm:6.1.0" + checksum: 10c0/a09f5a6b14ba8dcf57ae9a59474722e80f20406c53a61e9aedb0eedc693b135113ffe2983f4efc4b5065ae639442e9ae88df24941ef159c218b231011d733746 + languageName: node + linkType: hard + +"csso@npm:^4.2.0": + version: 4.2.0 + resolution: "csso@npm:4.2.0" + dependencies: + css-tree: "npm:^1.1.2" + checksum: 10c0/f8c6b1300efaa0f8855a7905ae3794a29c6496e7f16a71dec31eb6ca7cfb1f058a4b03fd39b66c4deac6cb06bf6b4ba86da7b67d7320389cb9994d52b924b903 + languageName: node + linkType: hard + +"cvmts-repo@workspace:.": + version: 0.0.0-use.local + resolution: "cvmts-repo@workspace:." + dependencies: + "@parcel/packager-ts": "npm:2.12.0" + "@parcel/transformer-sass": "npm:2.12.0" + "@parcel/transformer-typescript-types": "npm:2.12.0" + "@types/node": "npm:^20.14.10" + just-install: "npm:^2.0.1" + parcel: "npm:^2.12.0" + prettier: "npm:^3.2.5" + typescript: "npm:^5.4.4" + languageName: unknown + linkType: soft + +"data-uri-to-buffer@npm:^4.0.0": + version: 4.0.1 + resolution: "data-uri-to-buffer@npm:4.0.1" + checksum: 10c0/20a6b93107597530d71d4cb285acee17f66bcdfc03fd81040921a81252f19db27588d87fc8fc69e1950c55cfb0bf8ae40d0e5e21d907230813eb5d5a7f9eb45b + languageName: node + linkType: hard + +"dateformat@npm:^4.6.3": + version: 4.6.3 + resolution: "dateformat@npm:4.6.3" + checksum: 10c0/e2023b905e8cfe2eb8444fb558562b524807a51cdfe712570f360f873271600b5c94aebffaf11efb285e2c072264a7cf243eadb68f3eba0f8cc85fb86cd25df6 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.4": + version: 4.3.4 + resolution: "debug@npm:4.3.4" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/cedbec45298dd5c501d01b92b119cd3faebe5438c3917ff11ae1bff86a6c722930ac9c8659792824013168ba6db7c4668225d845c633fbdafbbf902a6389f736 + languageName: node + linkType: hard + +"debug@npm:^4.1.1": + version: 4.3.5 + resolution: "debug@npm:4.3.5" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/082c375a2bdc4f4469c99f325ff458adad62a3fc2c482d59923c260cb08152f34e2659f72b3767db8bb2f21ca81a60a42d1019605a412132d7b9f59363a005cc + languageName: node + linkType: hard + +"detect-libc@npm:^1.0.3": + version: 1.0.3 + resolution: "detect-libc@npm:1.0.3" + bin: + detect-libc: ./bin/detect-libc.js + checksum: 10c0/4da0deae9f69e13bc37a0902d78bf7169480004b1fed3c19722d56cff578d16f0e11633b7fbf5fb6249181236c72e90024cbd68f0b9558ae06e281f47326d50d + languageName: node + linkType: hard + +"detect-libc@npm:^2.0.1, detect-libc@npm:^2.0.3": + version: 2.0.3 + resolution: "detect-libc@npm:2.0.3" + checksum: 10c0/88095bda8f90220c95f162bf92cad70bd0e424913e655c20578600e35b91edc261af27531cf160a331e185c0ced93944bc7e09939143225f56312d7fd800fdb7 + languageName: node + linkType: hard + +"dom-serializer@npm:^1.0.1": + version: 1.4.1 + resolution: "dom-serializer@npm:1.4.1" + dependencies: + domelementtype: "npm:^2.0.1" + domhandler: "npm:^4.2.0" + entities: "npm:^2.0.0" + checksum: 10c0/67d775fa1ea3de52035c98168ddcd59418356943b5eccb80e3c8b3da53adb8e37edb2cc2f885802b7b1765bf5022aec21dfc32910d7f9e6de4c3148f095ab5e0 + languageName: node + linkType: hard + +"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0": + version: 2.3.0 + resolution: "domelementtype@npm:2.3.0" + checksum: 10c0/686f5a9ef0fff078c1412c05db73a0dce096190036f33e400a07e2a4518e9f56b1e324f5c576a0a747ef0e75b5d985c040b0d51945ce780c0dd3c625a18cd8c9 + languageName: node + linkType: hard + +"domhandler@npm:^4.2.0, domhandler@npm:^4.2.2, domhandler@npm:^4.3.1": + version: 4.3.1 + resolution: "domhandler@npm:4.3.1" + dependencies: + domelementtype: "npm:^2.2.0" + checksum: 10c0/5c199c7468cb052a8b5ab80b13528f0db3d794c64fc050ba793b574e158e67c93f8336e87fd81e9d5ee43b0e04aea4d8b93ed7be4899cb726a1601b3ba18538b + languageName: node + linkType: hard + +"domutils@npm:^2.8.0": + version: 2.8.0 + resolution: "domutils@npm:2.8.0" + dependencies: + dom-serializer: "npm:^1.0.1" + domelementtype: "npm:^2.2.0" + domhandler: "npm:^4.2.0" + checksum: 10c0/d58e2ae01922f0dd55894e61d18119924d88091837887bf1438f2327f32c65eb76426bd9384f81e7d6dcfb048e0f83c19b222ad7101176ad68cdc9c695b563db + languageName: node + linkType: hard + +"dotenv-expand@npm:^5.1.0": + version: 5.1.0 + resolution: "dotenv-expand@npm:5.1.0" + checksum: 10c0/24ac633de853ef474d0421cc639328b7134109c8dc2baaa5e3afb7495af5e9237136d7e6971e55668e4dce915487eb140967cdd2b3e99aa439e0f6bf8b56faeb + languageName: node + linkType: hard + +"dotenv@npm:^7.0.0": + version: 7.0.0 + resolution: "dotenv@npm:7.0.0" + checksum: 10c0/4d834d09d23ebd284e701c4204172659a7dcd51116f11c29c575ae6d918ccd4760a3383bdfd83cfbed42f061266b787f8e56452b952638867ea5476be875eb27 + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 10c0/26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 + languageName: node + linkType: hard + +"electron-to-chromium@npm:^1.4.668": + version: 1.4.746 + resolution: "electron-to-chromium@npm:1.4.746" + checksum: 10c0/1ff47105510e9a6dbc542d7165b88e030c8f2c815b30683ca05d8bc1a24e8f03e57caa0dc03959b08860b0465f9645edea5c682400da5b79b71ce9ddbb89a3d6 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 10c0/af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: 10c0/36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + languageName: node + linkType: hard + +"end-of-stream@npm:^1.1.0": + version: 1.4.4 + resolution: "end-of-stream@npm:1.4.4" + dependencies: + once: "npm:^1.4.0" + checksum: 10c0/870b423afb2d54bb8d243c63e07c170409d41e20b47eeef0727547aea5740bd6717aca45597a9f2745525667a6b804c1e7bede41f856818faee5806dd9ff3975 + languageName: node + linkType: hard + +"entities@npm:^2.0.0": + version: 2.2.0 + resolution: "entities@npm:2.2.0" + checksum: 10c0/7fba6af1f116300d2ba1c5673fc218af1961b20908638391b4e1e6d5850314ee2ac3ec22d741b3a8060479911c99305164aed19b6254bde75e7e6b1b2c3f3aa3 + languageName: node + linkType: hard + +"entities@npm:^3.0.1": + version: 3.0.1 + resolution: "entities@npm:3.0.1" + checksum: 10c0/2d93f48fd86de0b0ed8ee34456aa47b4e74a916a5e663cfcc7048302e2c7e932002926daf5a00ad6d5691e3c90673a15d413704d86d7e1b9532f9bc00d975590 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 10c0/b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + languageName: node + linkType: hard + +"error-ex@npm:^1.3.1": + version: 1.3.2 + resolution: "error-ex@npm:1.3.2" + dependencies: + is-arrayish: "npm:^0.2.1" + checksum: 10c0/ba827f89369b4c93382cfca5a264d059dfefdaa56ecc5e338ffa58a6471f5ed93b71a20add1d52290a4873d92381174382658c885ac1a2305f7baca363ce9cce + languageName: node + linkType: hard + +"escalade@npm:^3.1.1": + version: 3.1.2 + resolution: "escalade@npm:3.1.2" + checksum: 10c0/6b4adafecd0682f3aa1cd1106b8fff30e492c7015b178bc81b2d2f75106dabea6c6d6e8508fc491bd58e597c74abb0e8e2368f943ecb9393d4162e3c2f3cf287 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^1.0.5": + version: 1.0.5 + resolution: "escape-string-regexp@npm:1.0.5" + checksum: 10c0/a968ad453dd0c2724e14a4f20e177aaf32bb384ab41b674a8454afe9a41c5e6fe8903323e0a1052f56289d04bd600f81278edf140b0fcc02f5cac98d0f5b5371 + languageName: node + linkType: hard + +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 10c0/0255d9f936215fd206156fd4caa9e8d35e62075d720dc7d847e89b417e5e62cf1ce6c9b4e0a1633a9256de0efefaf9f8d26924b1f3c8620cffb9db78e7d3076b + languageName: node + linkType: hard + +"events@npm:^3.3.0": + version: 3.3.0 + resolution: "events@npm:3.3.0" + checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 + languageName: node + linkType: hard + +"execa@npm:^8.0.1": + version: 8.0.1 + resolution: "execa@npm:8.0.1" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^8.0.1" + human-signals: "npm:^5.0.0" + is-stream: "npm:^3.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^5.1.0" + onetime: "npm:^6.0.0" + signal-exit: "npm:^4.1.0" + strip-final-newline: "npm:^3.0.0" + checksum: 10c0/2c52d8775f5bf103ce8eec9c7ab3059909ba350a5164744e9947ed14a53f51687c040a250bda833f906d1283aa8803975b84e6c8f7a7c42f99dc8ef80250d1af + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 10c0/160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 + languageName: node + linkType: hard + +"extract-zip@npm:^2.0.1": + version: 2.0.1 + resolution: "extract-zip@npm:2.0.1" + dependencies: + "@types/yauzl": "npm:^2.9.1" + debug: "npm:^4.1.1" + get-stream: "npm:^5.1.0" + yauzl: "npm:^2.10.0" + dependenciesMeta: + "@types/yauzl": + optional: true + bin: + extract-zip: cli.js + checksum: 10c0/9afbd46854aa15a857ae0341a63a92743a7b89c8779102c3b4ffc207516b2019337353962309f85c66ee3d9092202a83cdc26dbf449a11981272038443974aee + languageName: node + linkType: hard + +"extsprintf@npm:1.3.0": + version: 1.3.0 + resolution: "extsprintf@npm:1.3.0" + checksum: 10c0/f75114a8388f0cbce68e277b6495dc3930db4dde1611072e4a140c24e204affd77320d004b947a132e9a3b97b8253017b2b62dce661975fb0adced707abf1ab5 + languageName: node + linkType: hard + +"extsprintf@npm:^1.2.0": + version: 1.4.1 + resolution: "extsprintf@npm:1.4.1" + checksum: 10c0/e10e2769985d0e9b6c7199b053a9957589d02e84de42832c295798cb422a025e6d4a92e0259c1fb4d07090f5bfde6b55fd9f880ac5855bd61d775f8ab75a7ab0 + languageName: node + linkType: hard + +"fast-copy@npm:^3.0.2": + version: 3.0.2 + resolution: "fast-copy@npm:3.0.2" + checksum: 10c0/02e8b9fd03c8c024d2987760ce126456a0e17470850b51e11a1c3254eed6832e4733ded2d93316c82bc0b36aeb991ad1ff48d1ba95effe7add7c3ab8d8eb554a + languageName: node + linkType: hard + +"fast-redact@npm:^3.1.1": + version: 3.5.0 + resolution: "fast-redact@npm:3.5.0" + checksum: 10c0/7e2ce4aad6e7535e0775bf12bd3e4f2e53d8051d8b630e0fa9e67f68cb0b0e6070d2f7a94b1d0522ef07e32f7c7cda5755e2b677a6538f1e9070ca053c42343a + languageName: node + linkType: hard + +"fast-safe-stringify@npm:^2.1.1": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: 10c0/d90ec1c963394919828872f21edaa3ad6f1dddd288d2bd4e977027afff09f5db40f94e39536d4646f7e01761d704d72d51dce5af1b93717f3489ef808f5f4e4d + languageName: node + linkType: hard + +"fd-slicer@npm:~1.1.0": + version: 1.1.0 + resolution: "fd-slicer@npm:1.1.0" + dependencies: + pend: "npm:~1.2.0" + checksum: 10c0/304dd70270298e3ffe3bcc05e6f7ade2511acc278bc52d025f8918b48b6aa3b77f10361bddfadfe2a28163f7af7adbdce96f4d22c31b2f648ba2901f0c5fc20e + languageName: node + linkType: hard + +"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": + version: 3.2.0 + resolution: "fetch-blob@npm:3.2.0" + dependencies: + node-domexception: "npm:^1.0.0" + web-streams-polyfill: "npm:^3.0.3" + checksum: 10c0/60054bf47bfa10fb0ba6cb7742acec2f37c1f56344f79a70bb8b1c48d77675927c720ff3191fa546410a0442c998d27ab05e9144c32d530d8a52fbe68f843b69 + languageName: node + linkType: hard + +"fill-range@npm:^7.0.1": + version: 7.0.1 + resolution: "fill-range@npm:7.0.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: 10c0/7cdad7d426ffbaadf45aeb5d15ec675bbd77f7597ad5399e3d2766987ed20bda24d5fac64b3ee79d93276f5865608bb22344a26b9b1ae6c4d00bd94bf611623f + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: "npm:^7.0.0" + signal-exit: "npm:^4.0.1" + checksum: 10c0/9700a0285628abaeb37007c9a4d92bd49f67210f09067638774338e146c8e9c825c5c877f072b2f75f41dc6a2d0be8664f79ffc03f6576649f54a84fb9b47de0 + languageName: node + linkType: hard + +"formdata-polyfill@npm:^4.0.10": + version: 4.0.10 + resolution: "formdata-polyfill@npm:4.0.10" + dependencies: + fetch-blob: "npm:^3.1.2" + checksum: 10c0/5392ec484f9ce0d5e0d52fb5a78e7486637d516179b0eb84d81389d7eccf9ca2f663079da56f761355c0a65792810e3b345dc24db9a8bbbcf24ef3c8c88570c6 + languageName: node + linkType: hard + +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + languageName: node + linkType: hard + +"fsevents@npm:~2.3.2": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"get-port@npm:^4.2.0": + version: 4.2.0 + resolution: "get-port@npm:4.2.0" + checksum: 10c0/ecce4233b720e7c6612aedc334ee8bb62b7d44db7ad6a55e58f7b3a17993ecfcb1bb218b8bb1ee197d0971c12e420aad2b3f95a93e4a117f2186f926ebcd2d42 + languageName: node + linkType: hard + +"get-stream@npm:^5.1.0": + version: 5.2.0 + resolution: "get-stream@npm:5.2.0" + dependencies: + pump: "npm:^3.0.0" + checksum: 10c0/43797ffd815fbb26685bf188c8cfebecb8af87b3925091dd7b9a9c915993293d78e3c9e1bce125928ff92f2d0796f3889b92b5ec6d58d1041b574682132e0a80 + languageName: node + linkType: hard + +"get-stream@npm:^8.0.1": + version: 8.0.1 + resolution: "get-stream@npm:8.0.1" + checksum: 10c0/5c2181e98202b9dae0bb4a849979291043e5892eb40312b47f0c22b9414fc9b28a3b6063d2375705eb24abc41ecf97894d9a51f64ff021511b504477b27b4290 + languageName: node + linkType: hard + +"glob-parent@npm:~5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: "npm:^4.0.1" + checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee + languageName: node + linkType: hard + +"glob@npm:^10.2.2, glob@npm:^10.3.10": + version: 10.3.12 + resolution: "glob@npm:10.3.12" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^2.3.6" + minimatch: "npm:^9.0.1" + minipass: "npm:^7.0.4" + path-scurry: "npm:^1.10.2" + bin: + glob: dist/esm/bin.mjs + checksum: 10c0/f60cefdc1cf3f958b2bb5823e1b233727f04916d489dc4641d76914f016e6704421e06a83cbb68b0cb1cb9382298b7a88075b844ad2127fc9727ea22b18b0711 + languageName: node + linkType: hard + +"globals@npm:^13.2.0": + version: 13.24.0 + resolution: "globals@npm:13.24.0" + dependencies: + type-fest: "npm:^0.20.2" + checksum: 10c0/d3c11aeea898eb83d5ec7a99508600fbe8f83d2cf00cbb77f873dbf2bcb39428eff1b538e4915c993d8a3b3473fa71eeebfe22c9bb3a3003d1e26b1f2c8a42cd + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + languageName: node + linkType: hard + +"has-flag@npm:^3.0.0": + version: 3.0.0 + resolution: "has-flag@npm:3.0.0" + checksum: 10c0/1c6c83b14b8b1b3c25b0727b8ba3e3b647f99e9e6e13eb7322107261de07a4c1be56fc0d45678fc376e09772a3a1642ccdaf8fc69bdf123b6c086598397ce473 + languageName: node + linkType: hard + +"has-flag@npm:^4.0.0": + version: 4.0.0 + resolution: "has-flag@npm:4.0.0" + checksum: 10c0/2e789c61b7888d66993e14e8331449e525ef42aac53c627cc53d1c3334e768bcb6abdc4f5f0de1478a25beec6f0bd62c7549058b7ac53e924040d4f301f02fd1 + languageName: node + linkType: hard + +"help-me@npm:^5.0.0": + version: 5.0.0 + resolution: "help-me@npm:5.0.0" + checksum: 10c0/054c0e2e9ae2231c85ab5e04f75109b9d068ffcc54e58fb22079822a5ace8ff3d02c66fd45379c902ad5ab825e5d2e1451fcc2f7eab1eb49e7d488133ba4cacb + languageName: node + linkType: hard + +"htmlnano@npm:^2.0.0": + version: 2.1.0 + resolution: "htmlnano@npm:2.1.0" + dependencies: + cosmiconfig: "npm:^8.0.0" + posthtml: "npm:^0.16.5" + timsort: "npm:^0.3.0" + peerDependencies: + cssnano: ^6.0.0 + postcss: ^8.3.11 + purgecss: ^5.0.0 + relateurl: ^0.2.7 + srcset: 4.0.0 + svgo: ^3.0.2 + terser: ^5.10.0 + uncss: ^0.17.3 + peerDependenciesMeta: + cssnano: + optional: true + postcss: + optional: true + purgecss: + optional: true + relateurl: + optional: true + srcset: + optional: true + svgo: + optional: true + terser: + optional: true + uncss: + optional: true + checksum: 10c0/33e78a18e044c6db671626babfdab60bd483c432164e6e38ef70c895a5698a91215972ebf2dbd7f7f8c05fbac80fa169ee1dde4bc0f1427d7dc3c162e0300610 + languageName: node + linkType: hard + +"htmlparser2@npm:^7.1.1": + version: 7.2.0 + resolution: "htmlparser2@npm:7.2.0" + dependencies: + domelementtype: "npm:^2.0.1" + domhandler: "npm:^4.2.2" + domutils: "npm:^2.8.0" + entities: "npm:^3.0.1" + checksum: 10c0/7e1fa7f3b2635f2a1c5272765e25aab33b241d84a43e9d27f28a0b7166b51a8025dec40a6a29af38d6a698a2f1d2983cb43e5c61d4e07ec5aa9df672a7460e16 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 10c0/ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.4 + resolution: "https-proxy-agent@npm:7.0.4" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:4" + checksum: 10c0/bc4f7c38da32a5fc622450b6cb49a24ff596f9bd48dcedb52d2da3fa1c1a80e100fb506bd59b326c012f21c863c69b275c23de1a01d0b84db396822fdf25e52b + languageName: node + linkType: hard + +"human-signals@npm:^5.0.0": + version: 5.0.0 + resolution: "human-signals@npm:5.0.0" + checksum: 10c0/5a9359073fe17a8b58e5a085e9a39a950366d9f00217c4ff5878bd312e09d80f460536ea6a3f260b5943a01fe55c158d1cea3fc7bee3d0520aeef04f6d915c82 + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + languageName: node + linkType: hard + +"ieee754@npm:^1.2.1": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb + languageName: node + linkType: hard + +"immutable@npm:^4.0.0": + version: 4.3.5 + resolution: "immutable@npm:4.3.5" + checksum: 10c0/63d2d7908241a955d18c7822fd2215b6e89ff5a1a33cc72cd475b013cbbdef7a705aa5170a51ce9f84a57f62fdddfaa34e7b5a14b33d8a43c65cc6a881d6e894 + languageName: node + linkType: hard + +"import-fresh@npm:^3.3.0": + version: 3.3.0 + resolution: "import-fresh@npm:3.3.0" + dependencies: + parent-module: "npm:^1.0.0" + resolve-from: "npm:^4.0.0" + checksum: 10c0/7f882953aa6b740d1f0e384d0547158bc86efbf2eea0f1483b8900a6f65c5a5123c2cf09b0d542cc419d0b98a759ecaeb394237e97ea427f2da221dc3cd80cc3 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + languageName: node + linkType: hard + +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: "npm:1.1.0" + sprintf-js: "npm:^1.1.3" + checksum: 10c0/331cd07fafcb3b24100613e4b53e1a2b4feab11e671e655d46dc09ee233da5011284d09ca40c4ecbdfe1d0004f462958675c224a804259f2f78d2465a87824bc + languageName: node + linkType: hard + +"ip6addr@npm:^0.2.5": + version: 0.2.5 + resolution: "ip6addr@npm:0.2.5" + dependencies: + assert-plus: "npm:^1.0.0" + jsprim: "npm:^2.0.2" + checksum: 10c0/aaa16f844d57d2c8afca375dabb42a62e6990ea044e397bf50e18bea8b445ae0978df6fae5898c898edfd6b58cc3d3c557f405a34792739be912cd303563a916 + languageName: node + linkType: hard + +"is-arrayish@npm:^0.2.1": + version: 0.2.1 + resolution: "is-arrayish@npm:0.2.1" + checksum: 10c0/e7fb686a739068bb70f860b39b67afc62acc62e36bb61c5f965768abce1873b379c563e61dd2adad96ebb7edf6651111b385e490cf508378959b0ed4cac4e729 + languageName: node + linkType: hard + +"is-arrayish@npm:^0.3.1": + version: 0.3.2 + resolution: "is-arrayish@npm:0.3.2" + checksum: 10c0/f59b43dc1d129edb6f0e282595e56477f98c40278a2acdc8b0a5c57097c9eff8fe55470493df5775478cf32a4dc8eaf6d3a749f07ceee5bc263a78b2434f6a54 + languageName: node + linkType: hard + +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: "npm:^2.0.0" + checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + languageName: node + linkType: hard + +"is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: "npm:^2.1.1" + checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a + languageName: node + linkType: hard + +"is-json@npm:^2.0.1": + version: 2.0.1 + resolution: "is-json@npm:2.0.1" + checksum: 10c0/49233aa560396e6365186be2f3a4618bf8b8067c1a97f2a25b8de09a9d7f326985f0163508067abeae5a21c69594a2a537f0147a5c4050ef097c15964e994cb4 + languageName: node + linkType: hard + +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 10c0/85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: 10c0/b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811 + languageName: node + linkType: hard + +"is-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "is-stream@npm:3.0.0" + checksum: 10c0/eb2f7127af02ee9aa2a0237b730e47ac2de0d4e76a4a905a50a11557f2339df5765eaea4ceb8029f1efa978586abe776908720bfcb1900c20c6ec5145f6f29d8 + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + languageName: node + linkType: hard + +"jackspeak@npm:^2.3.6": + version: 2.3.6 + resolution: "jackspeak@npm:2.3.6" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 10c0/f01d8f972d894cd7638bc338e9ef5ddb86f7b208ce177a36d718eac96ec86638a6efa17d0221b10073e64b45edc2ce15340db9380b1f5d5c5d000cbc517dc111 + languageName: node + linkType: hard + +"joycon@npm:^3.1.1": + version: 3.1.1 + resolution: "joycon@npm:3.1.1" + checksum: 10c0/131fb1e98c9065d067fd49b6e685487ac4ad4d254191d7aa2c9e3b90f4e9ca70430c43cad001602bdbdabcf58717d3b5c5b7461c1bd8e39478c8de706b3fe6ae + languageName: node + linkType: hard + +"js-tokens@npm:^4.0.0": + version: 4.0.0 + resolution: "js-tokens@npm:4.0.0" + checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed + languageName: node + linkType: hard + +"js-yaml@npm:^4.1.0": + version: 4.1.0 + resolution: "js-yaml@npm:4.1.0" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: 10c0/184a24b4eaacfce40ad9074c64fd42ac83cf74d8c8cd137718d456ced75051229e5061b8633c3366b8aada17945a7a356b337828c19da92b51ae62126575018f + languageName: node + linkType: hard + +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 10c0/4f907fb78d7b712e11dea8c165fe0921f81a657d3443dde75359ed52eb2b5d33ce6773d97985a089f09a65edd80b11cb75c767b57ba47391fee4c969f7215c96 + languageName: node + linkType: hard + +"json-parse-even-better-errors@npm:^2.3.0": + version: 2.3.1 + resolution: "json-parse-even-better-errors@npm:2.3.1" + checksum: 10c0/140932564c8f0b88455432e0f33c4cb4086b8868e37524e07e723f4eaedb9425bdc2bafd71bd1d9765bd15fd1e2d126972bc83990f55c467168c228c24d665f3 + languageName: node + linkType: hard + +"json-schema@npm:0.4.0": + version: 0.4.0 + resolution: "json-schema@npm:0.4.0" + checksum: 10c0/d4a637ec1d83544857c1c163232f3da46912e971d5bf054ba44fdb88f07d8d359a462b4aec46f2745efbc57053365608d88bc1d7b1729f7b4fc3369765639ed3 + languageName: node + linkType: hard + +"json5@npm:^2.2.0, json5@npm:^2.2.1": + version: 2.2.3 + resolution: "json5@npm:2.2.3" + bin: + json5: lib/cli.js + checksum: 10c0/5a04eed94810fa55c5ea138b2f7a5c12b97c3750bc63d11e511dcecbfef758003861522a070c2272764ee0f4e3e323862f386945aeb5b85b87ee43f084ba586c + languageName: node + linkType: hard + +"jsprim@npm:^2.0.2": + version: 2.0.2 + resolution: "jsprim@npm:2.0.2" + dependencies: + assert-plus: "npm:1.0.0" + extsprintf: "npm:1.3.0" + json-schema: "npm:0.4.0" + verror: "npm:1.10.0" + checksum: 10c0/677be2d41df536c92c6d0114a492ef197084018cfbb1a3e10b1fa1aad889564b2e3a7baa6af7949cc2d73678f42368b0be165a26bd4e4de6883a30dd6a24e98d + languageName: node + linkType: hard + +"just-install@npm:^2.0.1": + version: 2.0.1 + resolution: "just-install@npm:2.0.1" + dependencies: + extract-zip: "npm:^2.0.1" + node-fetch: "npm:^3.3.2" + bin: + just: bin/just.js + just-install: install.js + checksum: 10c0/dd7ef462b498c7289d223da1e9e54f24b957e18d760b903e1b85cc964265c5130deb82fc77ea5bd5b08fda1330c397aa9863f448b70b3403283ff3351c1e6792 + languageName: node + linkType: hard + +"lightningcss-darwin-arm64@npm:1.24.1": + version: 1.24.1 + resolution: "lightningcss-darwin-arm64@npm:1.24.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-x64@npm:1.24.1": + version: 1.24.1 + resolution: "lightningcss-darwin-x64@npm:1.24.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-freebsd-x64@npm:1.24.1": + version: 1.24.1 + resolution: "lightningcss-freebsd-x64@npm:1.24.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-linux-arm-gnueabihf@npm:1.24.1": + version: 1.24.1 + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.24.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"lightningcss-linux-arm64-gnu@npm:1.24.1": + version: 1.24.1 + resolution: "lightningcss-linux-arm64-gnu@npm:1.24.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-arm64-musl@npm:1.24.1": + version: 1.24.1 + resolution: "lightningcss-linux-arm64-musl@npm:1.24.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-linux-x64-gnu@npm:1.24.1": + version: 1.24.1 + resolution: "lightningcss-linux-x64-gnu@npm:1.24.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-x64-musl@npm:1.24.1": + version: 1.24.1 + resolution: "lightningcss-linux-x64-musl@npm:1.24.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-win32-x64-msvc@npm:1.24.1": + version: 1.24.1 + resolution: "lightningcss-win32-x64-msvc@npm:1.24.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"lightningcss@npm:^1.22.1": + version: 1.24.1 + resolution: "lightningcss@npm:1.24.1" + dependencies: + detect-libc: "npm:^1.0.3" + lightningcss-darwin-arm64: "npm:1.24.1" + lightningcss-darwin-x64: "npm:1.24.1" + lightningcss-freebsd-x64: "npm:1.24.1" + lightningcss-linux-arm-gnueabihf: "npm:1.24.1" + lightningcss-linux-arm64-gnu: "npm:1.24.1" + lightningcss-linux-arm64-musl: "npm:1.24.1" + lightningcss-linux-x64-gnu: "npm:1.24.1" + lightningcss-linux-x64-musl: "npm:1.24.1" + lightningcss-win32-x64-msvc: "npm:1.24.1" + dependenciesMeta: + lightningcss-darwin-arm64: + optional: true + lightningcss-darwin-x64: + optional: true + lightningcss-freebsd-x64: + optional: true + lightningcss-linux-arm-gnueabihf: + optional: true + lightningcss-linux-arm64-gnu: + optional: true + lightningcss-linux-arm64-musl: + optional: true + lightningcss-linux-x64-gnu: + optional: true + lightningcss-linux-x64-musl: + optional: true + lightningcss-win32-x64-msvc: + optional: true + checksum: 10c0/6fe2cd1bc92d431195ecb8bb9ebb98fc69010c04436354e0493b0a955d81823e6a2b114a4518ab46ad4eefc10606b51ca157adce2909e09e63b21002ccca93d3 + languageName: node + linkType: hard + +"lines-and-columns@npm:^1.1.6": + version: 1.2.4 + resolution: "lines-and-columns@npm:1.2.4" + checksum: 10c0/3da6ee62d4cd9f03f5dc90b4df2540fb85b352081bee77fe4bbcd12c9000ead7f35e0a38b8d09a9bb99b13223446dd8689ff3c4959807620726d788701a83d2d + languageName: node + linkType: hard + +"lmdb@npm:2.8.5": + version: 2.8.5 + resolution: "lmdb@npm:2.8.5" + dependencies: + "@lmdb/lmdb-darwin-arm64": "npm:2.8.5" + "@lmdb/lmdb-darwin-x64": "npm:2.8.5" + "@lmdb/lmdb-linux-arm": "npm:2.8.5" + "@lmdb/lmdb-linux-arm64": "npm:2.8.5" + "@lmdb/lmdb-linux-x64": "npm:2.8.5" + "@lmdb/lmdb-win32-x64": "npm:2.8.5" + msgpackr: "npm:^1.9.5" + node-addon-api: "npm:^6.1.0" + node-gyp: "npm:latest" + node-gyp-build-optional-packages: "npm:5.1.1" + ordered-binary: "npm:^1.4.1" + weak-lru-cache: "npm:^1.2.2" + dependenciesMeta: + "@lmdb/lmdb-darwin-arm64": + optional: true + "@lmdb/lmdb-darwin-x64": + optional: true + "@lmdb/lmdb-linux-arm": + optional: true + "@lmdb/lmdb-linux-arm64": + optional: true + "@lmdb/lmdb-linux-x64": + optional: true + "@lmdb/lmdb-win32-x64": + optional: true + bin: + download-lmdb-prebuilds: bin/download-prebuilds.js + checksum: 10c0/5c95ae636611f32d3583b26bca0d4b0dc236378f785b5735420edda62f88ddacc17c7586d586779a49f3377422c85c3e0b416c4a47f1c21945f76f001551afc9 + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.2.0 + resolution: "lru-cache@npm:10.2.0" + checksum: 10c0/c9847612aa2daaef102d30542a8d6d9b2c2bb36581c1bf0dc3ebf5e5f3352c772a749e604afae2e46873b930a9e9523743faac4e5b937c576ab29196774712ee + languageName: node + linkType: hard + +"lru-cache@npm:^6.0.0": + version: 6.0.0 + resolution: "lru-cache@npm:6.0.0" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10c0/cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 + languageName: node + linkType: hard + +"make-fetch-happen@npm:^13.0.0": + version: 13.0.0 + resolution: "make-fetch-happen@npm:13.0.0" + dependencies: + "@npmcli/agent": "npm:^2.0.0" + cacache: "npm:^18.0.0" + http-cache-semantics: "npm:^4.1.1" + is-lambda: "npm:^1.0.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^3.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.3" + promise-retry: "npm:^2.0.1" + ssri: "npm:^10.0.0" + checksum: 10c0/43b9f6dcbc6fe8b8604cb6396957c3698857a15ba4dbc38284f7f0e61f248300585ef1eb8cc62df54e9c724af977e45b5cdfd88320ef7f53e45070ed3488da55 + languageName: node + linkType: hard + +"maxmind@npm:^4.2.0": + version: 4.3.20 + resolution: "maxmind@npm:4.3.20" + dependencies: + mmdb-lib: "npm:2.1.1" + tiny-lru: "npm:11.2.6" + checksum: 10c0/f21b366f7c2bf7f6853eeea52478e53dd1052ad75f6f45c270258d5ff023c5f4a85c577d6b1ebdb0a8734073e24df5ed66375cdac0c3159a8f8ae30c6535149d + languageName: node + linkType: hard + +"mdn-data@npm:2.0.14": + version: 2.0.14 + resolution: "mdn-data@npm:2.0.14" + checksum: 10c0/67241f8708c1e665a061d2b042d2d243366e93e5bf1f917693007f6d55111588b952dcbfd3ea9c2d0969fb754aad81b30fdcfdcc24546495fc3b24336b28d4bd + languageName: node + linkType: hard + +"merge-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-stream@npm:2.0.0" + checksum: 10c0/867fdbb30a6d58b011449b8885601ec1690c3e41c759ecd5a9d609094f7aed0096c37823ff4a7190ef0b8f22cc86beb7049196ff68c016e3b3c671d0dac91ce5 + languageName: node + linkType: hard + +"micromatch@npm:^4.0.5": + version: 4.0.5 + resolution: "micromatch@npm:4.0.5" + dependencies: + braces: "npm:^3.0.2" + picomatch: "npm:^2.3.1" + checksum: 10c0/3d6505b20f9fa804af5d8c596cb1c5e475b9b0cd05f652c5b56141cf941bd72adaeb7a436fda344235cef93a7f29b7472efc779fcdb83b478eab0867b95cdeff + languageName: node + linkType: hard + +"mimic-fn@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-fn@npm:4.0.0" + checksum: 10c0/de9cc32be9996fd941e512248338e43407f63f6d497abe8441fa33447d922e927de54d4cc3c1a3c6d652857acd770389d5a3823f311a744132760ce2be15ccbf + languageName: node + linkType: hard + +"minimatch@npm:^9.0.1": + version: 9.0.4 + resolution: "minimatch@npm:9.0.4" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10c0/2c16f21f50e64922864e560ff97c587d15fd491f65d92a677a344e970fe62aafdbeafe648965fa96d33c061b4d0eabfe0213466203dd793367e7f28658cf6414 + languageName: node + linkType: hard + +"minimist@npm:^1.2.6": + version: 1.2.8 + resolution: "minimist@npm:1.2.8" + checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.4 + resolution: "minipass-fetch@npm:3.0.4" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.1.2" + dependenciesMeta: + encoding: + optional: true + checksum: 10c0/1b63c1f3313e88eeac4689f1b71c9f086598db9a189400e3ee960c32ed89e06737fa23976c9305c2d57464fb3fcdc12749d3378805c9d6176f5569b0d0ee8a75 + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 10c0/a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4": + version: 7.0.4 + resolution: "minipass@npm:7.0.4" + checksum: 10c0/6c7370a6dfd257bf18222da581ba89a5eaedca10e158781232a8b5542a90547540b4b9b7e7f490e4cda43acfbd12e086f0453728ecf8c19e0ef6921bc5958ac5 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: 10c0/64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: 10c0/46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf + languageName: node + linkType: hard + +"mmdb-lib@npm:2.1.1": + version: 2.1.1 + resolution: "mmdb-lib@npm:2.1.1" + checksum: 10c0/675817303af64c21be02e9550ce885b6ffcc6fbbeae7959a189493ccf68c6b7bac74afa00376fd7a421ff2acd8f74f44fc7fd25aeed0675fc21dbc1a9d5df9f9 + languageName: node + linkType: hard + +"mnemonist@npm:^0.39.5": + version: 0.39.8 + resolution: "mnemonist@npm:0.39.8" + dependencies: + obliterator: "npm:^2.0.1" + checksum: 10c0/fa810768d290919c4ecd3f8ba5c8458bc45df08d1c72fac8f3897721cd90ab42ee1c642cc5208cfd649d40222998dc011127702117c0ca676f243cc80f42cc11 + languageName: node + linkType: hard + +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: 10c0/a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc + languageName: node + linkType: hard + +"msgpackr-extract@npm:^3.0.2": + version: 3.0.2 + resolution: "msgpackr-extract@npm:3.0.2" + dependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "npm:3.0.2" + "@msgpackr-extract/msgpackr-extract-darwin-x64": "npm:3.0.2" + "@msgpackr-extract/msgpackr-extract-linux-arm": "npm:3.0.2" + "@msgpackr-extract/msgpackr-extract-linux-arm64": "npm:3.0.2" + "@msgpackr-extract/msgpackr-extract-linux-x64": "npm:3.0.2" + "@msgpackr-extract/msgpackr-extract-win32-x64": "npm:3.0.2" + node-gyp: "npm:latest" + node-gyp-build-optional-packages: "npm:5.0.7" + dependenciesMeta: + "@msgpackr-extract/msgpackr-extract-darwin-arm64": + optional: true + "@msgpackr-extract/msgpackr-extract-darwin-x64": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-arm": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-arm64": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-x64": + optional: true + "@msgpackr-extract/msgpackr-extract-win32-x64": + optional: true + bin: + download-msgpackr-prebuilds: bin/download-prebuilds.js + checksum: 10c0/f14727e0121c241a11cf75824f87822c0a08d65e6b8eba8a3fbf26c7d7287ea3f8ca3ab76887fda781a203bd16e51705207d82593ba6f06abca3181c743a352d + languageName: node + linkType: hard + +"msgpackr@npm:^1.10.2": + version: 1.10.2 + resolution: "msgpackr@npm:1.10.2" + dependencies: + msgpackr-extract: "npm:^3.0.2" + dependenciesMeta: + msgpackr-extract: + optional: true + checksum: 10c0/eb0a47b3d32a3be92f7a5b1182a67e5d9bfd5668d1aed63d3df03480a06798311eea339319b442ffafe83de19d9f3c9c6ac4d9081af0c9f896599d766a53db20 + languageName: node + linkType: hard + +"msgpackr@npm:^1.9.5, msgpackr@npm:^1.9.9": + version: 1.10.1 + resolution: "msgpackr@npm:1.10.1" + dependencies: + msgpackr-extract: "npm:^3.0.2" + dependenciesMeta: + msgpackr-extract: + optional: true + checksum: 10c0/2e6ed91af89ec15d1e5595c5b837a4adcbb185b0fbd4773d728ced89ab4abbdd3401f6777b193d487d9807e1cb0cf3da1ba9a0bd2d5a553e22355cea84a36bab + languageName: node + linkType: hard + +"negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: 10c0/3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 + languageName: node + linkType: hard + +"node-addon-api@npm:^6.1.0": + version: 6.1.0 + resolution: "node-addon-api@npm:6.1.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/d2699c4ad15740fd31482a3b6fca789af7723ab9d393adc6ac45250faaee72edad8f0b10b2b9d087df0de93f1bdc16d97afdd179b26b9ebc9ed68b569faa4bac + languageName: node + linkType: hard + +"node-addon-api@npm:^7.0.0": + version: 7.1.0 + resolution: "node-addon-api@npm:7.1.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/2e096ab079e3c46d33b0e252386e9c239c352f7cc6d75363d9a3c00bdff34c1a5da170da861917512843f213c32d024ced9dc9552b968029786480d18727ec66 + languageName: node + linkType: hard + +"node-domexception@npm:^1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: 10c0/5e5d63cda29856402df9472335af4bb13875e1927ad3be861dc5ebde38917aecbf9ae337923777af52a48c426b70148815e890a5d72760f1b4d758cc671b1a2b + languageName: node + linkType: hard + +"node-fetch@npm:^3.3.2": + version: 3.3.2 + resolution: "node-fetch@npm:3.3.2" + dependencies: + data-uri-to-buffer: "npm:^4.0.0" + fetch-blob: "npm:^3.1.4" + formdata-polyfill: "npm:^4.0.10" + checksum: 10c0/f3d5e56190562221398c9f5750198b34cf6113aa304e34ee97c94fd300ec578b25b2c2906edba922050fce983338fde0d5d34fcb0fc3336ade5bd0e429ad7538 + languageName: node + linkType: hard + +"node-gyp-build-optional-packages@npm:5.0.7": + version: 5.0.7 + resolution: "node-gyp-build-optional-packages@npm:5.0.7" + bin: + node-gyp-build-optional-packages: bin.js + node-gyp-build-optional-packages-optional: optional.js + node-gyp-build-optional-packages-test: build-test.js + checksum: 10c0/e0edb57358dfa8e31c26b38310ddc5ae81d19fd13b3bf095c41215dfd6a033b1269b510c3ce5e73f7a4ed3d36f101ea47716ec75be38f5e31916d185e7f18905 + languageName: node + linkType: hard + +"node-gyp-build-optional-packages@npm:5.1.1": + version: 5.1.1 + resolution: "node-gyp-build-optional-packages@npm:5.1.1" + dependencies: + detect-libc: "npm:^2.0.1" + bin: + node-gyp-build-optional-packages: bin.js + node-gyp-build-optional-packages-optional: optional.js + node-gyp-build-optional-packages-test: build-test.js + checksum: 10c0/f9fad2061c48fb0fc90831cd11d6a7670d731d22a5b00c7d3441b43b4003543299ff64ff2729afe2cefd7d14928e560d469336e5bb00f613932ec2cd56b3665b + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 10.1.0 + resolution: "node-gyp@npm:10.1.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^13.0.0" + nopt: "npm:^7.0.0" + proc-log: "npm:^3.0.0" + semver: "npm:^7.3.5" + tar: "npm:^6.1.2" + which: "npm:^4.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10c0/9cc821111ca244a01fb7f054db7523ab0a0cd837f665267eb962eb87695d71fb1e681f9e21464cc2fd7c05530dc4c81b810bca1a88f7d7186909b74477491a3c + languageName: node + linkType: hard + +"node-releases@npm:^2.0.14": + version: 2.0.14 + resolution: "node-releases@npm:2.0.14" + checksum: 10c0/199fc93773ae70ec9969bc6d5ac5b2bbd6eb986ed1907d751f411fef3ede0e4bfdb45ceb43711f8078bea237b6036db8b1bf208f6ff2b70c7d615afd157f3ab9 + languageName: node + linkType: hard + +"nopt@npm:^7.0.0": + version: 7.2.0 + resolution: "nopt@npm:7.2.0" + dependencies: + abbrev: "npm:^2.0.0" + bin: + nopt: bin/nopt.js + checksum: 10c0/9bd7198df6f16eb29ff16892c77bcf7f0cc41f9fb5c26280ac0def2cf8cf319f3b821b3af83eba0e74c85807cc430a16efe0db58fe6ae1f41e69519f585b6aff + languageName: node + linkType: hard + +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": + version: 3.0.0 + resolution: "normalize-path@npm:3.0.0" + checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 + languageName: node + linkType: hard + +"npm-run-path@npm:^5.1.0": + version: 5.3.0 + resolution: "npm-run-path@npm:5.3.0" + dependencies: + path-key: "npm:^4.0.0" + checksum: 10c0/124df74820c40c2eb9a8612a254ea1d557ddfab1581c3e751f825e3e366d9f00b0d76a3c94ecd8398e7f3eee193018622677e95816e8491f0797b21e30b2deba + languageName: node + linkType: hard + +"nth-check@npm:^2.0.1": + version: 2.1.1 + resolution: "nth-check@npm:2.1.1" + dependencies: + boolbase: "npm:^1.0.0" + checksum: 10c0/5fee7ff309727763689cfad844d979aedd2204a817fbaaf0e1603794a7c20db28548d7b024692f953557df6ce4a0ee4ae46cd8ebd9b36cfb300b9226b567c479 + languageName: node + linkType: hard + +"nullthrows@npm:^1.1.1": + version: 1.1.1 + resolution: "nullthrows@npm:1.1.1" + checksum: 10c0/56f34bd7c3dcb3bd23481a277fa22918120459d3e9d95ca72976c72e9cac33a97483f0b95fc420e2eb546b9fe6db398273aba9a938650cdb8c98ee8f159dcb30 + languageName: node + linkType: hard + +"obliterator@npm:^2.0.1": + version: 2.0.4 + resolution: "obliterator@npm:2.0.4" + checksum: 10c0/ff2c10d4de7d62cd1d588b4d18dfc42f246c9e3a259f60d5716f7f88e5b3a3f79856b3207db96ec9a836a01d0958a21c15afa62a3f4e73a1e0b75f2c2f6bab40 + languageName: node + linkType: hard + +"on-exit-leak-free@npm:^2.1.0": + version: 2.1.2 + resolution: "on-exit-leak-free@npm:2.1.2" + checksum: 10c0/faea2e1c9d696ecee919026c32be8d6a633a7ac1240b3b87e944a380e8a11dc9c95c4a1f8fb0568de7ab8db3823e790f12bda45296b1d111e341aad3922a0570 + languageName: node + linkType: hard + +"once@npm:^1.3.1, once@npm:^1.4.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 + languageName: node + linkType: hard + +"onetime@npm:^6.0.0": + version: 6.0.0 + resolution: "onetime@npm:6.0.0" + dependencies: + mimic-fn: "npm:^4.0.0" + checksum: 10c0/4eef7c6abfef697dd4479345a4100c382d73c149d2d56170a54a07418c50816937ad09500e1ed1e79d235989d073a9bade8557122aee24f0576ecde0f392bb6c + languageName: node + linkType: hard + +"ordered-binary@npm:^1.4.1": + version: 1.5.1 + resolution: "ordered-binary@npm:1.5.1" + checksum: 10c0/fb4c74e07436d0bf33d3b537c18dccafb39a60750a64d8b8fbd55f0b0f8eb7dad710f663b9c2edd1d59e9a27e13b638099da901ecf1cc95cd40173f42cf70f9e + languageName: node + linkType: hard + +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: 10c0/592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 + languageName: node + linkType: hard + +"parcel@npm:^2.12.0": + version: 2.12.0 + resolution: "parcel@npm:2.12.0" + dependencies: + "@parcel/config-default": "npm:2.12.0" + "@parcel/core": "npm:2.12.0" + "@parcel/diagnostic": "npm:2.12.0" + "@parcel/events": "npm:2.12.0" + "@parcel/fs": "npm:2.12.0" + "@parcel/logger": "npm:2.12.0" + "@parcel/package-manager": "npm:2.12.0" + "@parcel/reporter-cli": "npm:2.12.0" + "@parcel/reporter-dev-server": "npm:2.12.0" + "@parcel/reporter-tracer": "npm:2.12.0" + "@parcel/utils": "npm:2.12.0" + chalk: "npm:^4.1.0" + commander: "npm:^7.0.0" + get-port: "npm:^4.2.0" + bin: + parcel: lib/bin.js + checksum: 10c0/1853858c22cb728d3e3f524df04fbdc42aa27a0c8a3a0dbe2314d618ac13a3fe81836ce1560cdfce17338f61ec238d9b616073c181ab77af56664a0221af1b2a + languageName: node + linkType: hard + +"parent-module@npm:^1.0.0": + version: 1.0.1 + resolution: "parent-module@npm:1.0.1" + dependencies: + callsites: "npm:^3.0.0" + checksum: 10c0/c63d6e80000d4babd11978e0d3fee386ca7752a02b035fd2435960ffaa7219dc42146f07069fb65e6e8bf1caef89daf9af7535a39bddf354d78bf50d8294f556 + languageName: node + linkType: hard + +"parse-json@npm:^5.2.0": + version: 5.2.0 + resolution: "parse-json@npm:5.2.0" + dependencies: + "@babel/code-frame": "npm:^7.0.0" + error-ex: "npm:^1.3.1" + json-parse-even-better-errors: "npm:^2.3.0" + lines-and-columns: "npm:^1.1.6" + checksum: 10c0/77947f2253005be7a12d858aedbafa09c9ae39eb4863adf330f7b416ca4f4a08132e453e08de2db46459256fb66afaac5ee758b44fe6541b7cdaf9d252e59585 + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + languageName: node + linkType: hard + +"path-key@npm:^4.0.0": + version: 4.0.0 + resolution: "path-key@npm:4.0.0" + checksum: 10c0/794efeef32863a65ac312f3c0b0a99f921f3e827ff63afa5cb09a377e202c262b671f7b3832a4e64731003fa94af0263713962d317b9887bd1e0c48a342efba3 + languageName: node + linkType: hard + +"path-scurry@npm:^1.10.2": + version: 1.10.2 + resolution: "path-scurry@npm:1.10.2" + dependencies: + lru-cache: "npm:^10.2.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: 10c0/d723777fbf9627f201e64656680f66ebd940957eebacf780e6cce1c2919c29c116678b2d7dbf8821b3a2caa758d125f4444005ccec886a25c8f324504e48e601 + languageName: node + linkType: hard + +"path-type@npm:^4.0.0": + version: 4.0.0 + resolution: "path-type@npm:4.0.0" + checksum: 10c0/666f6973f332f27581371efaf303fd6c272cc43c2057b37aa99e3643158c7e4b2626549555d88626e99ea9e046f82f32e41bbde5f1508547e9a11b149b52387c + languageName: node + linkType: hard + +"pend@npm:~1.2.0": + version: 1.2.0 + resolution: "pend@npm:1.2.0" + checksum: 10c0/8a87e63f7a4afcfb0f9f77b39bb92374afc723418b9cb716ee4257689224171002e07768eeade4ecd0e86f1fa3d8f022994219fb45634f2dbd78c6803e452458 + languageName: node + linkType: hard + +"picocolors@npm:^1.0.0": + version: 1.0.0 + resolution: "picocolors@npm:1.0.0" + checksum: 10c0/20a5b249e331c14479d94ec6817a182fd7a5680debae82705747b2db7ec50009a5f6648d0621c561b0572703f84dbef0858abcbd5856d3c5511426afcb1961f7 + languageName: node + linkType: hard + +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be + languageName: node + linkType: hard + +"pino-abstract-transport@npm:^1.0.0, pino-abstract-transport@npm:^1.2.0": + version: 1.2.0 + resolution: "pino-abstract-transport@npm:1.2.0" + dependencies: + readable-stream: "npm:^4.0.0" + split2: "npm:^4.0.0" + checksum: 10c0/b4ab59529b7a91f488440147fc58ee0827a6c1c5ca3627292339354b1381072c1a6bfa9b46d03ad27872589e8477ecf74da12cf286e1e6b665ac64a3b806bf07 + languageName: node + linkType: hard + +"pino-pretty@npm:^11.2.1": + version: 11.2.1 + resolution: "pino-pretty@npm:11.2.1" + dependencies: + colorette: "npm:^2.0.7" + dateformat: "npm:^4.6.3" + fast-copy: "npm:^3.0.2" + fast-safe-stringify: "npm:^2.1.1" + help-me: "npm:^5.0.0" + joycon: "npm:^3.1.1" + minimist: "npm:^1.2.6" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^1.0.0" + pump: "npm:^3.0.0" + readable-stream: "npm:^4.0.0" + secure-json-parse: "npm:^2.4.0" + sonic-boom: "npm:^4.0.1" + strip-json-comments: "npm:^3.1.1" + bin: + pino-pretty: bin.js + checksum: 10c0/6c7f15b5bf8a007c8b7157eae445675b13cd95097ffa512d5ebd661f9e7abd328fa27592b25708756a09f098f87cb03ca81837518cd725c16e3f801129b941d4 + languageName: node + linkType: hard + +"pino-std-serializers@npm:^7.0.0": + version: 7.0.0 + resolution: "pino-std-serializers@npm:7.0.0" + checksum: 10c0/73e694d542e8de94445a03a98396cf383306de41fd75ecc07085d57ed7a57896198508a0dec6eefad8d701044af21eb27253ccc352586a03cf0d4a0bd25b4133 + languageName: node + linkType: hard + +"pino@npm:^9.3.1": + version: 9.3.1 + resolution: "pino@npm:9.3.1" + dependencies: + atomic-sleep: "npm:^1.0.0" + fast-redact: "npm:^3.1.1" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^1.2.0" + pino-std-serializers: "npm:^7.0.0" + process-warning: "npm:^3.0.0" + quick-format-unescaped: "npm:^4.0.3" + real-require: "npm:^0.2.0" + safe-stable-stringify: "npm:^2.3.1" + sonic-boom: "npm:^4.0.1" + thread-stream: "npm:^3.0.0" + bin: + pino: bin.js + checksum: 10c0/ab1e81b3e5a91852136d80a592939883eeb81442e5d3a2c070bdbdeb47c5aaa297ead246530b10eb6d5ff59445f4645d1333d342f255d9f002f73aea843e74ee + languageName: node + linkType: hard + +"postcss-value-parser@npm:^4.2.0": + version: 4.2.0 + resolution: "postcss-value-parser@npm:4.2.0" + checksum: 10c0/f4142a4f56565f77c1831168e04e3effd9ffcc5aebaf0f538eee4b2d465adfd4b85a44257bb48418202a63806a7da7fe9f56c330aebb3cac898e46b4cbf49161 + languageName: node + linkType: hard + +"posthtml-parser@npm:^0.10.1": + version: 0.10.2 + resolution: "posthtml-parser@npm:0.10.2" + dependencies: + htmlparser2: "npm:^7.1.1" + checksum: 10c0/90c7c2e0892c18577a56a5dd60a54c40feb0be7c712a79f711e1730b5eea468f8d521d387af9f08d78e6bca9df613286c3ff8a95ac9426671cbe9021d7ec2ae5 + languageName: node + linkType: hard + +"posthtml-parser@npm:^0.11.0": + version: 0.11.0 + resolution: "posthtml-parser@npm:0.11.0" + dependencies: + htmlparser2: "npm:^7.1.1" + checksum: 10c0/89bf980a60124790f776a9f21aec0f154eba5412d16f0f3a95de7a53d31b9acb9264bf317ab40c080413e3018a8e65c86278e6e8c0731c8e0363418982ed4296 + languageName: node + linkType: hard + +"posthtml-render@npm:^3.0.0": + version: 3.0.0 + resolution: "posthtml-render@npm:3.0.0" + dependencies: + is-json: "npm:^2.0.1" + checksum: 10c0/7adb9c20d0908663019c3c2dede3f6cc8bd19c17c81a1f42a1d8772195be4e5252aeb72a764e92d3424aebfa8c5d35c7ef1ec25243a802d35897aa928858505b + languageName: node + linkType: hard + +"posthtml@npm:^0.16.4, posthtml@npm:^0.16.5": + version: 0.16.6 + resolution: "posthtml@npm:0.16.6" + dependencies: + posthtml-parser: "npm:^0.11.0" + posthtml-render: "npm:^3.0.0" + checksum: 10c0/0505cb70ece051206ffa932394181372be6390a974fd2f50e4e6fdd5d11e41feffba9a5f5e22809ca42899f79bd489d53ceac1d7ad0d782db9521b578e5b7f5a + languageName: node + linkType: hard + +"prettier@npm:^3.2.5": + version: 3.2.5 + resolution: "prettier@npm:3.2.5" + bin: + prettier: bin/prettier.cjs + checksum: 10c0/ea327f37a7d46f2324a34ad35292af2ad4c4c3c3355da07313339d7e554320f66f65f91e856add8530157a733c6c4a897dc41b577056be5c24c40f739f5ee8c6 + languageName: node + linkType: hard + +"proc-log@npm:^3.0.0": + version: 3.0.0 + resolution: "proc-log@npm:3.0.0" + checksum: 10c0/f66430e4ff947dbb996058f6fd22de2c66612ae1a89b097744e17fb18a4e8e7a86db99eda52ccf15e53f00b63f4ec0b0911581ff2aac0355b625c8eac509b0dc + languageName: node + linkType: hard + +"process-warning@npm:^3.0.0": + version: 3.0.0 + resolution: "process-warning@npm:3.0.0" + checksum: 10c0/60f3c8ddee586f0706c1e6cb5aa9c86df05774b9330d792d7c8851cf0031afd759d665404d07037e0b4901b55c44a423f07bdc465c63de07d8d23196bb403622 + languageName: node + linkType: hard + +"process@npm:^0.11.10": + version: 0.11.10 + resolution: "process@npm:0.11.10" + checksum: 10c0/40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3 + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 10c0/9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + languageName: node + linkType: hard + +"pump@npm:^3.0.0": + version: 3.0.0 + resolution: "pump@npm:3.0.0" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10c0/bbdeda4f747cdf47db97428f3a135728669e56a0ae5f354a9ac5b74556556f5446a46f720a8f14ca2ece5be9b4d5d23c346db02b555f46739934cc6c093a5478 + languageName: node + linkType: hard + +"quick-format-unescaped@npm:^4.0.3": + version: 4.0.4 + resolution: "quick-format-unescaped@npm:4.0.4" + checksum: 10c0/fe5acc6f775b172ca5b4373df26f7e4fd347975578199e7d74b2ae4077f0af05baa27d231de1e80e8f72d88275ccc6028568a7a8c9ee5e7368ace0e18eff93a4 + languageName: node + linkType: hard + +"react-error-overlay@npm:6.0.9": + version: 6.0.9 + resolution: "react-error-overlay@npm:6.0.9" + checksum: 10c0/02f51337f34589305f827249acb597446489794cc5b5e721a6260111325b56942a7471b76967cba304e797d7e4ef16dd0bd989c112dd0bb9586270df0d75a4a9 + languageName: node + linkType: hard + +"react-refresh@npm:^0.9.0": + version: 0.9.0 + resolution: "react-refresh@npm:0.9.0" + checksum: 10c0/fa20f605e19dc10342e5cec8dcbb88cd4a473d26a7ff0acf1f0402e78f94ec309837be07a3cc3646f88d19f9ed07fa13a275f4656b5e3ced8fa23ce488984609 + languageName: node + linkType: hard + +"readable-stream@npm:^4.0.0": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" + dependencies: + abort-controller: "npm:^3.0.0" + buffer: "npm:^6.0.3" + events: "npm:^3.3.0" + process: "npm:^0.11.10" + string_decoder: "npm:^1.3.0" + checksum: 10c0/a2c80e0e53aabd91d7df0330929e32d0a73219f9477dbbb18472f6fdd6a11a699fc5d172a1beff98d50eae4f1496c950ffa85b7cc2c4c196963f289a5f39275d + languageName: node + linkType: hard + +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: "npm:^2.2.1" + checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b + languageName: node + linkType: hard + +"real-require@npm:^0.2.0": + version: 0.2.0 + resolution: "real-require@npm:0.2.0" + checksum: 10c0/23eea5623642f0477412ef8b91acd3969015a1501ed34992ada0e3af521d3c865bb2fe4cdbfec5fe4b505f6d1ef6a03e5c3652520837a8c3b53decff7e74b6a0 + languageName: node + linkType: hard + +"regenerator-runtime@npm:^0.13.7": + version: 0.13.11 + resolution: "regenerator-runtime@npm:0.13.11" + checksum: 10c0/12b069dc774001fbb0014f6a28f11c09ebfe3c0d984d88c9bced77fdb6fedbacbca434d24da9ae9371bfbf23f754869307fb51a4c98a8b8b18e5ef748677ca24 + languageName: node + linkType: hard + +"resolve-from@npm:^4.0.0": + version: 4.0.0 + resolution: "resolve-from@npm:4.0.0" + checksum: 10c0/8408eec31a3112ef96e3746c37be7d64020cda07c03a920f5024e77290a218ea758b26ca9529fd7b1ad283947f34b2291c1c0f6aa0ed34acfdda9c6014c8d190 + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + languageName: node + linkType: hard + +"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 + languageName: node + linkType: hard + +"safe-stable-stringify@npm:^2.3.1": + version: 2.4.3 + resolution: "safe-stable-stringify@npm:2.4.3" + checksum: 10c0/81dede06b8f2ae794efd868b1e281e3c9000e57b39801c6c162267eb9efda17bd7a9eafa7379e1f1cacd528d4ced7c80d7460ad26f62ada7c9e01dec61b2e768 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + languageName: node + linkType: hard + +"sass@npm:^1.38.0": + version: 1.75.0 + resolution: "sass@npm:1.75.0" + dependencies: + chokidar: "npm:>=3.0.0 <4.0.0" + immutable: "npm:^4.0.0" + source-map-js: "npm:>=0.6.2 <2.0.0" + bin: + sass: sass.js + checksum: 10c0/1564ab2c8041c99a330cec93127fe8abcf65ac63eecb471610ed7f3126a2599a58b788a3a98eb8719f7f40b9b04e00c92bc9e11a9c2180ad582b8cba9fb030b0 + languageName: node + linkType: hard + +"secure-json-parse@npm:^2.4.0": + version: 2.7.0 + resolution: "secure-json-parse@npm:2.7.0" + checksum: 10c0/f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4 + languageName: node + linkType: hard + +"semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.6.0": + version: 7.6.0 + resolution: "semver@npm:7.6.0" + dependencies: + lru-cache: "npm:^6.0.0" + bin: + semver: bin/semver.js + checksum: 10c0/fbfe717094ace0aa8d6332d7ef5ce727259815bd8d8815700853f4faf23aacbd7192522f0dc5af6df52ef4fa85a355ebd2f5d39f554bd028200d6cf481ab9b53 + languageName: node + linkType: hard + +"sharp@npm:^0.33.3": + version: 0.33.3 + resolution: "sharp@npm:0.33.3" + dependencies: + "@img/sharp-darwin-arm64": "npm:0.33.3" + "@img/sharp-darwin-x64": "npm:0.33.3" + "@img/sharp-libvips-darwin-arm64": "npm:1.0.2" + "@img/sharp-libvips-darwin-x64": "npm:1.0.2" + "@img/sharp-libvips-linux-arm": "npm:1.0.2" + "@img/sharp-libvips-linux-arm64": "npm:1.0.2" + "@img/sharp-libvips-linux-s390x": "npm:1.0.2" + "@img/sharp-libvips-linux-x64": "npm:1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "npm:1.0.2" + "@img/sharp-linux-arm": "npm:0.33.3" + "@img/sharp-linux-arm64": "npm:0.33.3" + "@img/sharp-linux-s390x": "npm:0.33.3" + "@img/sharp-linux-x64": "npm:0.33.3" + "@img/sharp-linuxmusl-arm64": "npm:0.33.3" + "@img/sharp-linuxmusl-x64": "npm:0.33.3" + "@img/sharp-wasm32": "npm:0.33.3" + "@img/sharp-win32-ia32": "npm:0.33.3" + "@img/sharp-win32-x64": "npm:0.33.3" + color: "npm:^4.2.3" + detect-libc: "npm:^2.0.3" + semver: "npm:^7.6.0" + dependenciesMeta: + "@img/sharp-darwin-arm64": + optional: true + "@img/sharp-darwin-x64": + optional: true + "@img/sharp-libvips-darwin-arm64": + optional: true + "@img/sharp-libvips-darwin-x64": + optional: true + "@img/sharp-libvips-linux-arm": + optional: true + "@img/sharp-libvips-linux-arm64": + optional: true + "@img/sharp-libvips-linux-s390x": + optional: true + "@img/sharp-libvips-linux-x64": + optional: true + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + "@img/sharp-libvips-linuxmusl-x64": + optional: true + "@img/sharp-linux-arm": + optional: true + "@img/sharp-linux-arm64": + optional: true + "@img/sharp-linux-s390x": + optional: true + "@img/sharp-linux-x64": + optional: true + "@img/sharp-linuxmusl-arm64": + optional: true + "@img/sharp-linuxmusl-x64": + optional: true + "@img/sharp-wasm32": + optional: true + "@img/sharp-win32-ia32": + optional: true + "@img/sharp-win32-x64": + optional: true + checksum: 10c0/12f5203426595b4e64c807162a6d52358b591d25fbb414a51fe38861584759fba38485be951ed98d15be3dfe21f2def5336f78ca35bf8bbd22d88cc78ca03f2a + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + languageName: node + linkType: hard + +"simple-swizzle@npm:^0.2.2": + version: 0.2.2 + resolution: "simple-swizzle@npm:0.2.2" + dependencies: + is-arrayish: "npm:^0.3.1" + checksum: 10c0/df5e4662a8c750bdba69af4e8263c5d96fe4cd0f9fe4bdfa3cbdeb45d2e869dff640beaaeb1ef0e99db4d8d2ec92f85508c269f50c972174851bc1ae5bd64308 + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.3 + resolution: "socks-proxy-agent@npm:8.0.3" + dependencies: + agent-base: "npm:^7.1.1" + debug: "npm:^4.3.4" + socks: "npm:^2.7.1" + checksum: 10c0/4950529affd8ccd6951575e21c1b7be8531b24d924aa4df3ee32df506af34b618c4e50d261f4cc603f1bfd8d426915b7d629966c8ce45b05fb5ad8c8b9a6459d + languageName: node + linkType: hard + +"socks@npm:^2.7.1": + version: 2.8.3 + resolution: "socks@npm:2.8.3" + dependencies: + ip-address: "npm:^9.0.5" + smart-buffer: "npm:^4.2.0" + checksum: 10c0/d54a52bf9325165770b674a67241143a3d8b4e4c8884560c4e0e078aace2a728dffc7f70150660f51b85797c4e1a3b82f9b7aa25e0a0ceae1a243365da5c51a7 + languageName: node + linkType: hard + +"sonic-boom@npm:^4.0.1": + version: 4.0.1 + resolution: "sonic-boom@npm:4.0.1" + dependencies: + atomic-sleep: "npm:^1.0.0" + checksum: 10c0/7b467f2bc8af7ff60bf210382f21c59728cc4b769af9b62c31dd88723f5cc472752d2320736cc366acc7c765ddd5bec3072c033b0faf249923f576a7453ba9d3 + languageName: node + linkType: hard + +"source-map-js@npm:>=0.6.2 <2.0.0": + version: 1.2.0 + resolution: "source-map-js@npm:1.2.0" + checksum: 10c0/7e5f896ac10a3a50fe2898e5009c58ff0dc102dcb056ed27a354623a0ece8954d4b2649e1a1b2b52ef2e161d26f8859c7710350930751640e71e374fe2d321a4 + languageName: node + linkType: hard + +"source-map@npm:^0.6.1": + version: 0.6.1 + resolution: "source-map@npm:0.6.1" + checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + languageName: node + linkType: hard + +"split2@npm:^4.0.0": + version: 4.2.0 + resolution: "split2@npm:4.2.0" + checksum: 10c0/b292beb8ce9215f8c642bb68be6249c5a4c7f332fc8ecadae7be5cbdf1ea95addc95f0459ef2e7ad9d45fd1064698a097e4eb211c83e772b49bc0ee423e91534 + languageName: node + linkType: hard + +"sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: 10c0/09270dc4f30d479e666aee820eacd9e464215cdff53848b443964202bf4051490538e5dd1b42e1a65cf7296916ca17640aebf63dae9812749c7542ee5f288dec + languageName: node + linkType: hard + +"srcset@npm:4": + version: 4.0.0 + resolution: "srcset@npm:4.0.0" + checksum: 10c0/0685c3bd2423b33831734fb71560cd8784f024895e70ee2ac2c392e30047c27ffd9481e001950fb0503f4906bc3fe963145935604edad77944d09c9800990660 + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.5 + resolution: "ssri@npm:10.0.5" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/b091f2ae92474183c7ac5ed3f9811457e1df23df7a7e70c9476eaa9a0c4a0c8fc190fb45acefbf023ca9ee864dd6754237a697dc52a0fb182afe65d8e77443d8 + languageName: node + linkType: hard + +"stable@npm:^0.1.8": + version: 0.1.8 + resolution: "stable@npm:0.1.8" + checksum: 10c0/df74b5883075076e78f8e365e4068ecd977af6c09da510cfc3148a303d4b87bc9aa8f7c48feb67ed4ef970b6140bd9eabba2129e28024aa88df5ea0114cba39d + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: 10c0/1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca + languageName: node + linkType: hard + +"string_decoder@npm:^1.3.0": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: "npm:~5.2.0" + checksum: 10c0/810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 10c0/a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 + languageName: node + linkType: hard + +"strip-final-newline@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-final-newline@npm:3.0.0" + checksum: 10c0/a771a17901427bac6293fd416db7577e2bc1c34a19d38351e9d5478c3c415f523f391003b42ed475f27e33a78233035df183525395f731d3bfb8cdcbd4da08ce + languageName: node + linkType: hard + +"strip-json-comments@npm:^3.1.1": + version: 3.1.1 + resolution: "strip-json-comments@npm:3.1.1" + checksum: 10c0/9681a6257b925a7fa0f285851c0e613cc934a50661fa7bb41ca9cbbff89686bb4a0ee366e6ecedc4daafd01e83eee0720111ab294366fe7c185e935475ebcecd + languageName: node + linkType: hard + +"supports-color@npm:^5.3.0": + version: 5.5.0 + resolution: "supports-color@npm:5.5.0" + dependencies: + has-flag: "npm:^3.0.0" + checksum: 10c0/6ae5ff319bfbb021f8a86da8ea1f8db52fac8bd4d499492e30ec17095b58af11f0c55f8577390a749b1c4dde691b6a0315dab78f5f54c9b3d83f8fb5905c1c05 + languageName: node + linkType: hard + +"supports-color@npm:^7.1.0": + version: 7.2.0 + resolution: "supports-color@npm:7.2.0" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10c0/afb4c88521b8b136b5f5f95160c98dee7243dc79d5432db7efc27efb219385bbc7d9427398e43dd6cc730a0f87d5085ce1652af7efbe391327bc0a7d0f7fc124 + languageName: node + linkType: hard + +"svgo@npm:^2.4.0": + version: 2.8.0 + resolution: "svgo@npm:2.8.0" + dependencies: + "@trysound/sax": "npm:0.2.0" + commander: "npm:^7.2.0" + css-select: "npm:^4.1.3" + css-tree: "npm:^1.1.3" + csso: "npm:^4.2.0" + picocolors: "npm:^1.0.0" + stable: "npm:^0.1.8" + bin: + svgo: bin/svgo + checksum: 10c0/0741f5d5cad63111a90a0ce7a1a5a9013f6d293e871b75efe39addb57f29a263e45294e485a4d2ff9cc260a5d142c8b5937b2234b4ef05efdd2706fb2d360ecc + languageName: node + linkType: hard + +"tar@npm:^6.1.11, tar@npm:^6.1.2": + version: 6.2.1 + resolution: "tar@npm:6.2.1" + dependencies: + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 + languageName: node + linkType: hard + +"term-size@npm:^2.2.1": + version: 2.2.1 + resolution: "term-size@npm:2.2.1" + checksum: 10c0/89f6bba1d05d425156c0910982f9344d9e4aebf12d64bfa1f460d93c24baa7bc4c4a21d355fbd7153c316433df0538f64d0ae6e336cc4a69fdda4f85d62bc79d + languageName: node + linkType: hard + +"thread-stream@npm:^3.0.0": + version: 3.1.0 + resolution: "thread-stream@npm:3.1.0" + dependencies: + real-require: "npm:^0.2.0" + checksum: 10c0/c36118379940b77a6ef3e6f4d5dd31e97b8210c3f7b9a54eb8fe6358ab173f6d0acfaf69b9c3db024b948c0c5fd2a7df93e2e49151af02076b35ada3205ec9a6 + languageName: node + linkType: hard + +"timsort@npm:^0.3.0": + version: 0.3.0 + resolution: "timsort@npm:0.3.0" + checksum: 10c0/571b2054a0db3cf80eb255f8609a1f798cae9176f9ec6e3fbd03d64186c015cc9e1e75b88ba38e1d71aebcc03a931352522c7387dcb90caeb148375c7bc106f4 + languageName: node + linkType: hard + +"tiny-lru@npm:11.2.6": + version: 11.2.6 + resolution: "tiny-lru@npm:11.2.6" + checksum: 10c0/d59b2047edae1b4b79708070463ed27ddb1daa64563b74eedaa571e555c47f8de3a7cc19171f47dc46c01f1b7283d9afd2c682dddb4832552ed747d52cd297a6 + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: "npm:^7.0.0" + checksum: 10c0/487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892 + languageName: node + linkType: hard + +"toml@npm:^3.0.0": + version: 3.0.0 + resolution: "toml@npm:3.0.0" + checksum: 10c0/8d7ed3e700ca602e5419fca343e1c595eb7aa177745141f0761a5b20874b58ee5c878cd045c408da9d130cb2b611c639912210ba96ce2f78e443569aa8060c18 + languageName: node + linkType: hard + +"tslib@npm:^2.4.0": + version: 2.6.2 + resolution: "tslib@npm:2.6.2" + checksum: 10c0/e03a8a4271152c8b26604ed45535954c0a45296e32445b4b87f8a5abdb2421f40b59b4ca437c4346af0f28179780d604094eb64546bee2019d903d01c6c19bdb + languageName: node + linkType: hard + +"type-fest@npm:^0.20.2": + version: 0.20.2 + resolution: "type-fest@npm:0.20.2" + checksum: 10c0/dea9df45ea1f0aaa4e2d3bed3f9a0bfe9e5b2592bddb92eb1bf06e50bcf98dbb78189668cd8bc31a0511d3fc25539b4cd5c704497e53e93e2d40ca764b10bfc3 + languageName: node + linkType: hard + +"typescript@npm:>=3.0.0, typescript@npm:^5.4.4": + version: 5.4.5 + resolution: "typescript@npm:5.4.5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/2954022ada340fd3d6a9e2b8e534f65d57c92d5f3989a263754a78aba549f7e6529acc1921913560a4b816c46dce7df4a4d29f9f11a3dc0d4213bb76d043251e + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A>=3.0.0#optional!builtin, typescript@patch:typescript@npm%3A^5.4.4#optional!builtin": + version: 5.4.5 + resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin::version=5.4.5&hash=5adc0c" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/db2ad2a16ca829f50427eeb1da155e7a45e598eec7b086d8b4e8ba44e5a235f758e606d681c66992230d3fc3b8995865e5fd0b22a2c95486d0b3200f83072ec9 + languageName: node + linkType: hard + +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 10c0/bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501 + languageName: node + linkType: hard + +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: "npm:^4.0.0" + checksum: 10c0/6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10c0/cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 + languageName: node + linkType: hard + +"update-browserslist-db@npm:^1.0.13": + version: 1.0.13 + resolution: "update-browserslist-db@npm:1.0.13" + dependencies: + escalade: "npm:^3.1.1" + picocolors: "npm:^1.0.0" + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 10c0/e52b8b521c78ce1e0c775f356cd16a9c22c70d25f3e01180839c407a5dc787fb05a13f67560cbaf316770d26fa99f78f1acd711b1b54a4f35d4820d4ea7136e6 + languageName: node + linkType: hard + +"utility-types@npm:^3.10.0": + version: 3.11.0 + resolution: "utility-types@npm:3.11.0" + checksum: 10c0/2f1580137b0c3e6cf5405f37aaa8f5249961a76d26f1ca8efc0ff49a2fc0e0b2db56de8e521a174d075758e0c7eb3e590edec0832eb44478b958f09914920f19 + languageName: node + linkType: hard + +"verror@npm:1.10.0": + version: 1.10.0 + resolution: "verror@npm:1.10.0" + dependencies: + assert-plus: "npm:^1.0.0" + core-util-is: "npm:1.0.2" + extsprintf: "npm:^1.2.0" + checksum: 10c0/37ccdf8542b5863c525128908ac80f2b476eed36a32cb944de930ca1e2e78584cc435c4b9b4c68d0fc13a47b45ff364b4be43aa74f8804f9050140f660fb660d + languageName: node + linkType: hard + +"weak-lru-cache@npm:^1.2.2": + version: 1.2.2 + resolution: "weak-lru-cache@npm:1.2.2" + checksum: 10c0/744847bd5b96ca86db1cb40d0aea7e92c02bbdb05f501181bf9c581e82fa2afbda32a327ffbe75749302b8492ab449f1c657ca02410d725f5d412d1e6c607d72 + languageName: node + linkType: hard + +"web-streams-polyfill@npm:^3.0.3": + version: 3.3.3 + resolution: "web-streams-polyfill@npm:3.3.3" + checksum: 10c0/64e855c47f6c8330b5436147db1c75cb7e7474d924166800e8e2aab5eb6c76aac4981a84261dd2982b3e754490900b99791c80ae1407a9fa0dcff74f82ea3a7f + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + languageName: node + linkType: hard + +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" + dependencies: + isexe: "npm:^3.1.1" + bin: + node-which: bin/which.js + checksum: 10c0/449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 + languageName: node + linkType: hard + +"ws@npm:^8.14.1": + version: 8.16.0 + resolution: "ws@npm:8.16.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/a7783bb421c648b1e622b423409cb2a58ac5839521d2f689e84bc9dc41d59379c692dd405b15a997ea1d4c0c2e5314ad707332d0c558f15232d2bc07c0b4618a + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + languageName: node + linkType: hard + +"yauzl@npm:^2.10.0": + version: 2.10.0 + resolution: "yauzl@npm:2.10.0" + dependencies: + buffer-crc32: "npm:~0.2.3" + fd-slicer: "npm:~1.1.0" + checksum: 10c0/f265002af7541b9ec3589a27f5fb8f11cf348b53cc15e2751272e3c062cd73f3e715bc72d43257de71bbaecae446c3f1b14af7559e8ab0261625375541816422 + languageName: node + linkType: hard