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